pruning nonsense
parent
0b0b85fc85
commit
56d7bac084
|
@ -17,9 +17,6 @@ config :logger, :console,
|
|||
# Iex needs colors too.
|
||||
config :iex, :colors, enabled: true
|
||||
|
||||
# bot <-> firmware transports.
|
||||
config :farmbot, expected_fw_version: "4.0.2"
|
||||
|
||||
# Rollbar
|
||||
config :farmbot, rollbar_access_token: "dcd79b191ab84aa3b28259cbb80e2060"
|
||||
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
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 :farmbot, :init, [
|
||||
|
||||
]
|
||||
|
||||
|
||||
config :wobserver,
|
||||
mode: :plug,
|
||||
remote_url_prefix: "/wobserver"
|
||||
|
|
|
@ -1,18 +1 @@
|
|||
use Mix.Config
|
||||
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: ""
|
||||
|
|
|
@ -14,25 +14,23 @@ defmodule Farmbot do
|
|||
def start(type, args)
|
||||
def start(_, args) do
|
||||
Logger.info ">> Booting Farmbot OS version: #{@version} - #{@commit}"
|
||||
case Supervisor.start_link(__MODULE__, args, name: Farmbot) do
|
||||
case Supervisor.start_link(__MODULE__, args, [name: Farmbot]) do
|
||||
{:ok, pid} -> {:ok, pid}
|
||||
{:error, reason} -> Farmbot.System.factory_reset(reason)
|
||||
# {:error, reason} -> Farmbot.System.factory_reset(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
children = [
|
||||
supervisor(Registry, [:duplicate, Farmbot.Registry]),
|
||||
|
||||
supervisor(Farmbot.DebugLog.Supervisor, [args, [name: Farmbot.DebugLog ]]),
|
||||
supervisor(Farmbot.System.Supervisor, [args, [name: Farmbot.System.Supervisor ]]),
|
||||
supervisor(Farmbot.HTTP.Supervisor, [args, [name: Farmbot.HTTP.Supervisor ]]),
|
||||
supervisor(Farmbot.BotState.Supervisor, [args, [name: Farmbot.BotState.Supervisor ]]),
|
||||
supervisor(Farmbot.Transport.Supervisor, [args, [name: Farmbot.Transport.Supervisor ]]), # Can this be a child of the bot's state maybe?
|
||||
supervisor(Farmbot.FarmEvent.Supervisor, [args, [name: Farmbot.FarmEvent.Supervisor ]]), # maybe make this a child of the database?
|
||||
supervisor(Farmbot.Serial.Supervisor, [args, [name: Farmbot.Serial.Supervisor ]]),
|
||||
supervisor(Farmbot.Farmware.Supervisor, [args, [name: Farmbot.Farmware.Supervisor ]]),
|
||||
worker(Farmbot.ImageWatcher, [args, [name: Farmbot.ImageWatcher ]]), # this may need to move too.
|
||||
# supervisor(Farmbot.HTTP.Supervisor, [args, [name: Farmbot.HTTP.Supervisor ]]),
|
||||
# supervisor(Farmbot.BotState.Supervisor, [args, [name: Farmbot.BotState.Supervisor ]]),
|
||||
# supervisor(Farmbot.Transport.Supervisor, [args, [name: Farmbot.Transport.Supervisor ]]), # Can this be a child of the bot's state maybe?
|
||||
# supervisor(Farmbot.FarmEvent.Supervisor, [args, [name: Farmbot.FarmEvent.Supervisor ]]), # maybe make this a child of the database?
|
||||
# supervisor(Farmbot.Serial.Supervisor, [args, [name: Farmbot.Serial.Supervisor ]]),
|
||||
# supervisor(Farmbot.Farmware.Supervisor, [args, [name: Farmbot.Farmware.Supervisor ]]),
|
||||
# worker(Farmbot.ImageWatcher, [args, [name: Farmbot.ImageWatcher ]]), # this may need to move too.
|
||||
]
|
||||
opts = [strategy: :one_for_one]
|
||||
supervise(children, opts)
|
||||
|
|
|
@ -25,91 +25,13 @@ defmodule Farmbot.DebugLog do
|
|||
enables the `debug_log/1` function.
|
||||
"""
|
||||
defmacro __using__(opts) do
|
||||
color = Keyword.get(opts, :color)
|
||||
name = Keyword.get(opts, :name)
|
||||
ccolor = Keyword.get(opts, :color) || :NC
|
||||
quote do
|
||||
|
||||
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
|
||||
end # defmacro
|
||||
|
||||
defmodule Handler do
|
||||
@moduledoc """
|
||||
Handler for DebugLogger
|
||||
"""
|
||||
use GenEvent
|
||||
|
||||
@doc false
|
||||
defdelegate color(color), to: Farmbot.DebugLog
|
||||
|
||||
def init(state), do: {:ok, state}
|
||||
|
||||
def handle_event(_, :all), do: {:ok, :all}
|
||||
|
||||
def handle_event({module, {color, str}}, state) when is_binary(str) do
|
||||
filter_me? = Map.get(state, module)
|
||||
unless filter_me? do
|
||||
IO.puts "#{color(color)} [#{module}]#{color(:NC)} #{str}"
|
||||
end
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:filter, :all}, _state) do
|
||||
{:ok, :all}
|
||||
end
|
||||
|
||||
def handle_call({:filter, module}, state) do
|
||||
{:ok, :ok, Map.put(state, module, :filterme)}
|
||||
end
|
||||
|
||||
def handle_call({:unfilter, module}, state) do
|
||||
if state == :all do
|
||||
{:ok, :error, state}
|
||||
else
|
||||
{:ok, :ok, Map.delete(state, module)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start the Debug Logger
|
||||
"""
|
||||
def start_link(event_server) do
|
||||
:ok = GenEvent.add_handler(event_server, Handler, %{})
|
||||
{:ok, self()}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Filter a module from the handler.
|
||||
"""
|
||||
def filter(module) do
|
||||
GenEvent.call(__MODULE__, Handler, {:filter, module})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unfilter a module from the handler.
|
||||
"""
|
||||
def unfilter(module) do
|
||||
GenEvent.call(__MODULE__, Handler, {:unfilter, module})
|
||||
end
|
||||
end # defmodule
|
||||
import Farmbot.DebugLog
|
||||
def debug_log(msg) do
|
||||
module = __MODULE__ |> Module.split() |> Enum.take(-2)
|
||||
IO.puts "#{color(unquote(ccolor))}[#{module}] #{msg}#{color(:NC)}"
|
||||
end # debug log
|
||||
end # quote
|
||||
end # defmacro
|
||||
end # defmodule
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
defmodule Farmbot.DebugLog.Supervisor do
|
||||
@moduledoc "Internal Logger Supervisor"
|
||||
use Supervisor
|
||||
|
||||
def start_link(args, opts) do
|
||||
Supervisor.start_link(__MODULE__, args, opts)
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
children = [
|
||||
worker(GenEvent, [name: Farmbot.DebugLog])
|
||||
worker(Farmbot.DebugLog, [Farmbot.DebugLog])
|
||||
]
|
||||
opts = [strategy: :one_for_one]
|
||||
supervise(children, opts)
|
||||
end
|
||||
end
|
|
@ -1,187 +0,0 @@
|
|||
defmodule Farmbot.System.Network.Hostapd do
|
||||
@moduledoc """
|
||||
Manages an OS process of hostapd and DNSMASQ.
|
||||
"""
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [:hostapd, :dnsmasq, :interface, :ip_addr, :manager]
|
||||
end
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
@hostapd_conf_file "hostapd.conf"
|
||||
@hostapd_pid_file "hostapd.pid"
|
||||
|
||||
@dnsmasq_conf_file "dnsmasq.conf"
|
||||
@dnsmasq_pid_file "dnsmasq.pid"
|
||||
|
||||
@doc ~s"""
|
||||
Example:
|
||||
Iex> Hostapd.start_link ip_address: "192.168.24.1",
|
||||
...> manager: Farmbot.Network.Manager, interface: "wlan0"
|
||||
"""
|
||||
def start_link(
|
||||
[interface: interface, ip_address: ip_addr, manager: manager])
|
||||
do
|
||||
name = Module.concat([__MODULE__, interface])
|
||||
GenServer.start_link(__MODULE__,
|
||||
[interface: interface, ip_address: ip_addr, manager: manager],
|
||||
name: name)
|
||||
end
|
||||
|
||||
defp setup_hostapd(interface, ip_addr) do
|
||||
# HOSTAPD
|
||||
# Make sure the interface is in proper condition.
|
||||
:ok = hostapd_ip_settings_up(interface, ip_addr)
|
||||
# build the hostapd configuration
|
||||
hostapd_conf = build_hostapd_conf(interface, build_ssid())
|
||||
# build a config file
|
||||
File.mkdir! "/tmp/hostapd"
|
||||
File.write! "/tmp/hostapd/#{@hostapd_conf_file}", hostapd_conf
|
||||
hostapd_cmd = "hostapd -P /tmp/hostapd/#{@hostapd_pid_file} " <>
|
||||
"/tmp/hostapd/#{@hostapd_conf_file}"
|
||||
|
||||
hostapd_port = Port.open({:spawn, hostapd_cmd}, [:binary])
|
||||
hostapd_os_pid = hostapd_port|> Port.info() |> Keyword.get(:os_pid)
|
||||
{hostapd_port, hostapd_os_pid}
|
||||
end
|
||||
|
||||
defp setup_dnsmasq(ip_addr, interface) do
|
||||
# DNSMASQ
|
||||
dnsmasq_conf = build_dnsmasq_conf(ip_addr, interface)
|
||||
File.mkdir!("/tmp/dnsmasq")
|
||||
:ok = File.write("/tmp/dnsmasq/#{@dnsmasq_conf_file}", dnsmasq_conf)
|
||||
dnsmasq_cmd = "dnsmasq -k --dhcp-lease " <>
|
||||
"/tmp/dnsmasq/#{@dnsmasq_pid_file} " <>
|
||||
"--conf-dir=/tmp/dnsmasq"
|
||||
dnsmasq_port = Port.open({:spawn, dnsmasq_cmd}, [:binary])
|
||||
dnsmasq_os_pid = dnsmasq_port|> Port.info() |> Keyword.get(:os_pid)
|
||||
{dnsmasq_port, dnsmasq_os_pid}
|
||||
end
|
||||
|
||||
def init([interface: interface, ip_address: ip_addr, manager: manager]) do
|
||||
Logger.info ">> is starting hostapd on #{interface}"
|
||||
# We want to know if something does.
|
||||
Process.flag :trap_exit, true
|
||||
|
||||
{hostapd_port, hostapd_os_pid} = setup_hostapd(interface, ip_addr)
|
||||
{dnsmasq_port, dnsmasq_os_pid} = setup_dnsmasq(ip_addr, interface)
|
||||
|
||||
state = %State{
|
||||
hostapd: {hostapd_port, hostapd_os_pid},
|
||||
dnsmasq: {dnsmasq_port, dnsmasq_os_pid},
|
||||
interface: interface,
|
||||
ip_addr: ip_addr,
|
||||
manager: manager
|
||||
}
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defp hostapd_ip_settings_up(interface, ip_addr) do
|
||||
:ok =
|
||||
"ip" |> System.cmd(["link", "set", "#{interface}", "up"])
|
||||
|> print_cmd
|
||||
:ok =
|
||||
"ip" |> System.cmd(["addr", "add", "#{ip_addr}/24", "dev", "#{interface}"])
|
||||
|> print_cmd
|
||||
:ok
|
||||
end
|
||||
|
||||
defp hostapd_ip_settings_down(interface, ip_addr) do
|
||||
:ok =
|
||||
"ip" |> System.cmd(["link", "set", "#{interface}", "down"])
|
||||
|> print_cmd
|
||||
:ok =
|
||||
"ip" |> System.cmd(["addr", "del", "#{ip_addr}/24", "dev", "#{interface}"])
|
||||
|> print_cmd
|
||||
:ok =
|
||||
"ip" |> System.cmd(["link", "set", "#{interface}", "up"])
|
||||
|> print_cmd
|
||||
:ok
|
||||
end
|
||||
|
||||
defp build_hostapd_conf(interface, ssid) do
|
||||
"""
|
||||
interface=#{interface}
|
||||
ssid=#{ssid}
|
||||
hw_mode=g
|
||||
channel=6
|
||||
auth_algs=1
|
||||
wmm_enabled=0
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_ssid do
|
||||
node_str =
|
||||
node() |> Atom.to_string
|
||||
[name, "nerves-" <> id] =
|
||||
node_str |> String.split("@")
|
||||
name <> "-" <> id
|
||||
end
|
||||
|
||||
defp build_dnsmasq_conf(ip_addr, interface) do
|
||||
[a, b, c, _] = ip_addr |> String.split(".")
|
||||
first_part = "#{a}.#{b}.#{c}."
|
||||
"""
|
||||
interface=#{interface}
|
||||
dhcp-range=#{first_part}50,#{first_part}250,2h
|
||||
dhcp-option=3,#{ip_addr}
|
||||
dhcp-option=6,#{ip_addr}
|
||||
dhcp-authoritative
|
||||
address=/#/#{ip_addr}
|
||||
server=/farmbot/#{ip_addr}
|
||||
local=/farmbot/
|
||||
domain=farmbot
|
||||
"""
|
||||
end
|
||||
|
||||
defp kill(os_pid),
|
||||
do: :ok = "kill" |> System.cmd(["15", "#{os_pid}"]) |> print_cmd
|
||||
|
||||
defp print_cmd({_, 0}), do: :ok
|
||||
defp print_cmd({res, num}) do
|
||||
Logger.error ">> encountered an error (#{num}): #{res}"
|
||||
:error
|
||||
end
|
||||
|
||||
def handle_info({port, {:data, data}}, state) do
|
||||
{hostapd_port, _} = state.hostapd
|
||||
{dnsmasq_port, _} = state.dnsmasq
|
||||
cond do
|
||||
port == hostapd_port ->
|
||||
handle_hostapd(data, state)
|
||||
port == dnsmasq_port ->
|
||||
handle_dnsmasq(data, state)
|
||||
true -> {:noreply, state}
|
||||
end
|
||||
end
|
||||
def handle_info(_thing, state), do: {:noreply, state}
|
||||
|
||||
defp handle_hostapd(data, state) when is_bitstring(data) do
|
||||
GenEvent.notify(state.manager, {:hostapd, String.trim(data)})
|
||||
{:noreply, state}
|
||||
end
|
||||
defp handle_hostapd(_, state), do: {:noreply, state}
|
||||
|
||||
defp handle_dnsmasq(data, state) when is_bitstring(data) do
|
||||
GenEvent.notify(state.manager, {:dnsmasq, String.trim(data)})
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp handle_dnsmasq(_, state), do: {:noreply, state}
|
||||
|
||||
def terminate(_, state) do
|
||||
Logger.info ">> is stopping hostapd"
|
||||
{_hostapd_port, hostapd_pid} = state.hostapd
|
||||
{_dnsmasq_port, dnsmasq_pid} = state.dnsmasq
|
||||
# Port.close hostapd_port
|
||||
# Port.close dnsmasq_port
|
||||
:ok = kill(hostapd_pid)
|
||||
:ok = kill(dnsmasq_pid)
|
||||
hostapd_ip_settings_down(state.interface, state.ip_addr)
|
||||
File.rm_rf! "/tmp/hostapd"
|
||||
File.rm_rf! "/tmp/dnsmasq"
|
||||
end
|
||||
end
|
|
@ -1,56 +0,0 @@
|
|||
defmodule Farmbot.System.Network.Ntp do
|
||||
@moduledoc """
|
||||
Sets time.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias Farmbot.{DebugLog}
|
||||
use DebugLog
|
||||
|
||||
@doc """
|
||||
Tries to set the time from ntp.
|
||||
This will try 3 times to set the time. if it fails the thrid time,
|
||||
it will return an error
|
||||
"""
|
||||
@spec set_time(integer) :: :ok | {:error, term}
|
||||
def set_time(tries \\ 0)
|
||||
def set_time(tries) when tries < 4 do
|
||||
Process.sleep(1000 * tries)
|
||||
case :inet_res.gethostbyname('0.pool.ntp.org') do
|
||||
{:ok, {:hostent, _url, _, _, _, _}} ->
|
||||
do_try_set_time(tries)
|
||||
{:error, err} ->
|
||||
debug_log "Failed to set time (#{tries}): DNS Lookup: #{inspect err}"
|
||||
set_time(tries + 1)
|
||||
end
|
||||
end
|
||||
|
||||
def set_time(_), do: {:error, :timeout}
|
||||
|
||||
defp do_try_set_time(tries) when tries < 4 do
|
||||
# we try to set ntp time 3 times before giving up.
|
||||
Logger.info ">> trying to set time (try #{tries})"
|
||||
:os.cmd('ntpd -p 0.pool.ntp.org -p 1.pool.ntp.org')
|
||||
wait_for_time(tries)
|
||||
end
|
||||
|
||||
defp wait_for_time(tries, loops \\ 0)
|
||||
|
||||
defp wait_for_time(tries, loops) when loops > 5 do
|
||||
:os.cmd('killall ntpd')
|
||||
set_time(tries)
|
||||
end
|
||||
|
||||
defp wait_for_time(tries, loops) do
|
||||
case :os.system_time do
|
||||
t when t > 1_474_929 ->
|
||||
Logger.flush()
|
||||
Logger.info ">> Time is set.", type: :success
|
||||
:ok
|
||||
_ ->
|
||||
Process.sleep(1_000 * loops)
|
||||
wait_for_time(tries, loops + 1)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -1,76 +0,0 @@
|
|||
defmodule Farmbot.System.FS.ConfigFileMigrations do
|
||||
@moduledoc """
|
||||
Migrates a state map to the current form.
|
||||
|
||||
# Heres how they work
|
||||
* this module lists all the files in "#{:code.priv_dir(:farmbot)}/migrations"
|
||||
* it sorts them in order of dates.
|
||||
* those files are just elixir script files `*.exs`
|
||||
* said file must export ONE module, with one function function named `run\1`
|
||||
* it takes a json map, and must return that map, but updated
|
||||
* when the migration runs successfully, it writes a file under the same name, but with `.migrated` at the
|
||||
_end of the file name, so we know not to migrate this again.
|
||||
* does this for every file in the list, passing the last one to the next one.
|
||||
|
||||
# example
|
||||
* we needed to change "steps_per_mm" from an integer, to a map: `%{x: int, y: int, z: int}`
|
||||
* so we took the version with an int, and changed it, and returned the new verion. Simple.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
@save_dir Application.get_env(:farmbot, :path)
|
||||
|
||||
@doc """
|
||||
Does the migrations
|
||||
"""
|
||||
def migrate(json_map) do
|
||||
list_of_files = get_migrations()
|
||||
Enum.reduce(list_of_files, json_map, fn(file, json) ->
|
||||
migrated = "#{@save_dir}/#{file}.migrated"
|
||||
# if there is no <timestamp>-<description>_migration.exs.migrated file
|
||||
# run the migration
|
||||
|
||||
# Check the file existance.
|
||||
if File.exists?(migrated) do
|
||||
# if we don't run a migration, just take the accumulator
|
||||
json
|
||||
else
|
||||
# Run the migration
|
||||
Logger.info ">> running config migration: #{file}"
|
||||
{{:module, m, _s, _}, _} = Code.eval_file file, migrations_dir()
|
||||
|
||||
# TODO(Connor): Find out why m.run can return nil?
|
||||
next = m.run(json) || %{}
|
||||
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
# write the contents of this migration, to the .migrated file.
|
||||
File.write(migrated, Poison.encode!(next))
|
||||
Logger.info ">> #{file} migration complete", type: :success
|
||||
end, true
|
||||
|
||||
# merge the current acc, with the just run migration
|
||||
Map.merge(json, next)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
returns a list of file sorted by date
|
||||
"""
|
||||
@spec get_migrations :: [String.t]
|
||||
def get_migrations do
|
||||
migrations_dir()
|
||||
|> File.ls!
|
||||
|> Enum.sort(fn(migration, last) ->
|
||||
[time_stampa, _desca] = String.split(migration, "-")
|
||||
int_time_stampa = String.to_integer(time_stampa)
|
||||
|
||||
[time_stampb, _descb] = String.split(last, "-")
|
||||
int_time_stampb = String.to_integer(time_stampb)
|
||||
int_time_stampa <= int_time_stampb
|
||||
end)
|
||||
end
|
||||
|
||||
defp migrations_dir, do: "#{:code.priv_dir(:farmbot)}/migrations"
|
||||
|
||||
end
|
|
@ -1,134 +0,0 @@
|
|||
defmodule Farmbot.System.FS.ConfigStorage do
|
||||
@moduledoc """
|
||||
Loads information according to a configuration JSON file.
|
||||
This does not handle the configuration file, it just holds all the
|
||||
information in an easy to reach place.
|
||||
"""
|
||||
use GenServer
|
||||
alias Farmbot.System.FS.ConfigFileMigrations, as: CFM
|
||||
require Logger
|
||||
alias Farmbot.Context
|
||||
|
||||
@config_file Application.get_env(:farmbot, :path) <> "/config.json"
|
||||
@default_config_file_name Application.get_env(:farmbot, :config_file_name)
|
||||
defp default_config_file,
|
||||
do: "#{:code.priv_dir(:farmbot)}/configs/#{@default_config_file_name}"
|
||||
|
||||
def start_link(%Context{} = _ctx, opts) do
|
||||
GenServer.start_link(__MODULE__, @config_file, opts)
|
||||
end
|
||||
|
||||
def init(path) do
|
||||
Logger.info ">> Config Storage init!"
|
||||
# Checks if the json file exists or not
|
||||
case File.read(path) do
|
||||
# if it does parse it
|
||||
{:ok, contents} ->
|
||||
Logger.info ">> is loading its configuration file: #{path}"
|
||||
f = parse_json!(contents)
|
||||
migrated = CFM.migrate(f)
|
||||
# NOTE(Connor):
|
||||
# this will write the initial config file, the migrated one, or
|
||||
# the current one at every boot.
|
||||
# seems ineffecient but oh well
|
||||
write!(migrated)
|
||||
Logger.info ">> no migrations"
|
||||
{:ok, migrated}
|
||||
# if not start over with the default config file (from the priv dir)
|
||||
{:error, :enoent} ->
|
||||
Logger.info ">> is creating a new configuration file: #{default_config_file()}"
|
||||
reset_config()
|
||||
init(@config_file)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_config do
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
File.cp!(default_config_file(), @config_file)
|
||||
end
|
||||
end
|
||||
|
||||
def read_config_file do
|
||||
GenServer.call(__MODULE__, :read_config_file)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Replace the configuration json with a new one.
|
||||
BE CAREFUL IM NOT CHECKING THE FILE AT ALL
|
||||
"""
|
||||
@spec replace_config_file(binary | map) :: :ok | {:error, term}
|
||||
def replace_config_file(config) when is_map(config) do
|
||||
# PLEASE FIXME: This needs to be better validated.
|
||||
GenServer.call(__MODULE__, {:replace_config_file, config})
|
||||
end
|
||||
|
||||
def replace_config_file(config) when is_binary(config) do
|
||||
f = parse_json!(config)
|
||||
GenServer.call(__MODULE__, {:replace_config_file, f})
|
||||
end
|
||||
|
||||
def handle_call(:read_config_file, _, state) do
|
||||
read = File.read(@config_file)
|
||||
{:reply, read, state}
|
||||
end
|
||||
|
||||
def handle_call({:replace_config_file, new_state}, _, _old_state) do
|
||||
write!(:ok, new_state)
|
||||
end
|
||||
|
||||
def handle_call({:get, module, :all}, _, state) do
|
||||
m = module_to_key(module)
|
||||
f = state |> Map.get(m)
|
||||
{:reply, {:ok, f}, state}
|
||||
end
|
||||
|
||||
# GenServer.call(ConfigStorage, {:get, Blah, :uh})
|
||||
def handle_call({:get, module, key}, _, state) do
|
||||
m = module_to_key(module)
|
||||
case state[m] do
|
||||
nil -> {:reply, {:error, :bad_module}, state}
|
||||
# this might be a HACK, or it might be clever
|
||||
false -> {:reply, {:ok, nil}, state}
|
||||
config -> {:reply, {:ok, config[key]}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:put, module, {key, val}}, state) do
|
||||
m = module_to_key(module)
|
||||
old = Map.get(state, m)
|
||||
new = Map.put(old, key, val)
|
||||
new_state = Map.put(state, m, new)
|
||||
write! new_state
|
||||
end
|
||||
|
||||
def terminate(_, _), do: nil
|
||||
|
||||
defp module_to_key(module),
|
||||
do: module
|
||||
|> Module.split
|
||||
|> List.last
|
||||
|> String.Casing.downcase
|
||||
|
||||
@spec write!(map) :: {:noreply, map}
|
||||
defp write!(state) do
|
||||
do_write(state)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@spec write!(any, map) :: {:reply, any, map}
|
||||
defp write!(reply, state) do
|
||||
do_write(state)
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
defp do_write(state) do
|
||||
json = Poison.encode!(state)
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
File.write!(@config_file, json)
|
||||
end
|
||||
end
|
||||
|
||||
# tries to parse contents. raises an exception if it can't
|
||||
@spec parse_json!(binary) :: map
|
||||
defp parse_json!(contents), do: contents |> Poison.decode!
|
||||
end
|
|
@ -1,107 +0,0 @@
|
|||
defmodule Farmbot.System.FS do
|
||||
@moduledoc """
|
||||
Handles filesystem reads and writes and formatting.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
use GenStage
|
||||
@path Application.get_env(:farmbot, :path)
|
||||
alias Farmbot.Context
|
||||
|
||||
def start_link(%Context{} = _ctx, target, opts),
|
||||
do: GenStage.start_link(__MODULE__, target, opts)
|
||||
|
||||
def init(target) do
|
||||
Logger.info ">> #{target} FileSystem Init"
|
||||
mod = Module.concat([Farmbot, System, target, FileSystem])
|
||||
mod.fs_init
|
||||
mod.mount_read_only
|
||||
spawn __MODULE__, :check_update_file, []
|
||||
{:producer, []}
|
||||
end
|
||||
|
||||
def check_update_file do
|
||||
update_file_path = "#{path()}/.post_update"
|
||||
case File.stat(update_file_path) do
|
||||
{:ok, _file} ->
|
||||
Logger.info "We are in post update mode!"
|
||||
result = Farmbot.System.Updates.post_install()
|
||||
transaction fn ->
|
||||
Logger.info "Cleaning up post update mode!", type: :busy
|
||||
File.rm!(update_file_path)
|
||||
end, true
|
||||
if result == :reboot do
|
||||
Farmbot.System.reboot()
|
||||
end
|
||||
_ ->
|
||||
Logger.info "Not in post update mode!"
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~s"""
|
||||
Safely executes FileSystem commands.\n
|
||||
* mounts the file system as read_write
|
||||
* executes function
|
||||
* mounts the file system as read_only again.
|
||||
Example:
|
||||
Iex> transaction fn() -> File.write("/state/bs.txt" , "hey") end
|
||||
"""
|
||||
@spec transaction(function) :: :ok | nil
|
||||
def transaction(function, block? \\ false, timeout \\ 10_000)
|
||||
def transaction(fun, false, _timeout) when is_function(fun) do
|
||||
# HACK(Connor) i dont want to handle two different :add_transactions
|
||||
GenServer.call(__MODULE__, {:add_transaction, fun, __MODULE__})
|
||||
end
|
||||
|
||||
def transaction(fun, true, timeout) when is_function(fun) do
|
||||
task = Task.async(fn() ->
|
||||
GenServer.call(__MODULE__, {:add_transaction, fun, self()})
|
||||
# FIXME(Connor) this is probably.. well terrible
|
||||
receive do
|
||||
thing -> thing
|
||||
end
|
||||
end)
|
||||
|
||||
case Task.yield(task, timeout) || Task.shutdown(task) do
|
||||
{:ok, result} ->
|
||||
result
|
||||
nil ->
|
||||
Logger.error "Failed to execute FS transaction in #{timeout} ms"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_state :: [any]
|
||||
def get_state, do: GenServer.call(__MODULE__, :get_state)
|
||||
|
||||
def handle_call(:get_state, _, state), do: {:reply, [], state, state}
|
||||
|
||||
# where there is no other queued up stuff
|
||||
def handle_call({:add_transaction, fun, pid}, _from, []) do
|
||||
{:reply, :ok, [{fun, pid}], []}
|
||||
end
|
||||
|
||||
def handle_call({:add_transaction, fun, pid}, _from, queue) do
|
||||
Logger.info ">> is queueing a fs transaction"
|
||||
{:reply, :ok, [], [{fun, pid} | queue]}
|
||||
end
|
||||
|
||||
def handle_demand(demand, queue) when demand > 0 do
|
||||
trans = Enum.reverse(queue)
|
||||
{:noreply, trans, []}
|
||||
end
|
||||
|
||||
def handle_info(_, state), do: {:noreply, [], state}
|
||||
|
||||
@doc """
|
||||
Returns the path where farmbot keeps its persistant data.
|
||||
"""
|
||||
def path, do: @path
|
||||
|
||||
# BEHAVIOR
|
||||
@type return_type :: :ok | {:error, term}
|
||||
@callback mount_read_only() :: return_type
|
||||
@callback mount_read_write() :: return_type
|
||||
@callback fs_init() :: return_type
|
||||
end
|
|
@ -1,48 +0,0 @@
|
|||
defmodule Farmbot.System.FS.Worker do
|
||||
@moduledoc """
|
||||
Pulls transactions from the queue, does them, etc.
|
||||
"""
|
||||
|
||||
# NOTE(Connor) This isn't working the way i want it to, but i dont have time
|
||||
# to fix it right now.
|
||||
# Expected behavior is for this module to get the list of transactions,
|
||||
# make the filesystem read-write, do all the things
|
||||
# and then make the fs read-only again.
|
||||
# What actually happens is it grabs them one at a time for some reason
|
||||
# Either way this seems to have fixed the race condition waiting for
|
||||
# teh transaction and mucking up state.
|
||||
|
||||
require Logger
|
||||
use GenStage
|
||||
alias Farmbot.Context
|
||||
|
||||
def start_link(%Context{} = _ctx, target, opts) do
|
||||
GenStage.start_link(__MODULE__, target, opts)
|
||||
end
|
||||
|
||||
def init(target) do
|
||||
mod = Module.concat([Farmbot, System, target, FileSystem])
|
||||
{:consumer, mod, subscribe_to: [Farmbot.System.FS]}
|
||||
end
|
||||
|
||||
def handle_events(events, _from, mod) do
|
||||
mod.mount_read_write
|
||||
for {transaction, cb} <- events do
|
||||
transaction.()
|
||||
if is_pid(cb) do
|
||||
send cb, {:ok, :ok}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
mod.mount_read_only
|
||||
{:noreply, [], mod}
|
||||
end
|
||||
|
||||
def handle_call(:get_state, _, state), do: {:reply, [], state, state}
|
||||
|
||||
def terminate(_, mod) do
|
||||
mod.mount_read_only
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Farmbot.System.Init do
|
||||
@moduledoc "Lets write init.d in Elixir!"
|
||||
|
||||
@doc "OTP Spec."
|
||||
@spec fb_init(atom, [term]) :: Supervisor.Spec.spec
|
||||
def fb_init(module, args) do
|
||||
import Supervisor.Spec
|
||||
supervisor(module, args, [restart: :permanent])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start an init module.
|
||||
returning {:error, reason} will factory reset the system.
|
||||
"""
|
||||
@callback start_link(term, Supervisor.options) :: Supervisor.supervisor
|
||||
end
|
|
@ -1,337 +0,0 @@
|
|||
defmodule Farmbot.System.Network do
|
||||
@moduledoc """
|
||||
Network functionality.
|
||||
"""
|
||||
require Logger
|
||||
alias Farmbot.System.FS.ConfigStorage, as: CS
|
||||
alias Farmbot.System.Network.Ntp
|
||||
alias Farmbot.{Context, Auth, HTTP, DebugLog}
|
||||
use DebugLog
|
||||
use GenServer
|
||||
|
||||
@type netman :: pid
|
||||
|
||||
@spec mod(atom | binary | Context.t) :: atom
|
||||
defp mod(%Context{} = context), do: mod(context.network)
|
||||
defp mod(target) when is_atom(target) or is_binary(target),
|
||||
do: Module.concat([Farmbot, System, target, Network])
|
||||
|
||||
@doc """
|
||||
Starts the network manager
|
||||
"""
|
||||
def start_link(%Context{} = context, target, opts) do
|
||||
GenServer.start_link(__MODULE__, [context, target], opts)
|
||||
end
|
||||
|
||||
def init([%Context{} = context, target]) do
|
||||
Logger.info ">> is starting networking"
|
||||
m = mod(target)
|
||||
{:ok, _cb} = m.start_link(context)
|
||||
{:ok, interface_config} = get_config("interfaces")
|
||||
parse_and_start_config(context, interface_config, m)
|
||||
{:ok, %{context: context, target: target}}
|
||||
end
|
||||
|
||||
# if networking is disabled.
|
||||
defp parse_and_start_config(%Context{} = context, nil, _), do: spawn(fn ->
|
||||
Process.sleep(2000)
|
||||
spawn fn ->
|
||||
maybe_get_fpf(context)
|
||||
end
|
||||
maybe_log_in(context)
|
||||
end)
|
||||
|
||||
defp parse_and_start_config(%Context{} = context, config, m) do
|
||||
for {interface, settings} <- config do
|
||||
m.start_interface(context, interface, settings)
|
||||
end
|
||||
end
|
||||
|
||||
case Mix.env do
|
||||
:test -> defp maybe_log_in(_ctx), do: :ok
|
||||
_ -> defp maybe_log_in(ctx), do: Farmbot.Auth.try_log_in(ctx.auth)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Scans for wireless ssids.
|
||||
"""
|
||||
@spec scan(netman, binary) :: [binary]
|
||||
def scan(netman, interface_name) do
|
||||
GenServer.call(netman, {:scan, interface_name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all network interfaces that Farmbot Detected.
|
||||
"""
|
||||
@spec enumerate(netman) :: [binary] | {:error, term}
|
||||
def enumerate(netman) do
|
||||
GenServer.call(netman, :enumerate)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Restarts networking services. This will block.
|
||||
"""
|
||||
def restart(netman) when is_atom(netman) or is_pid(netman) do
|
||||
stop_all(netman)
|
||||
{:ok, interface_config} = get_config("interfaces")
|
||||
m = mod(get_mod(netman)) # wtf is this?
|
||||
context = get_context(netman)
|
||||
parse_and_start_config(context, interface_config, m)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts an interface
|
||||
"""
|
||||
def start_interface(netman, interface, settings) do
|
||||
GenServer.call(netman, {:start, interface, settings}, :infinity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops an interface
|
||||
"""
|
||||
def stop_interface(netman, interface) do
|
||||
GenServer.call(netman, {:stop, interface}, :infinity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops all interfaces
|
||||
"""
|
||||
def stop_all(netman) do
|
||||
{:ok, interfaces} = get_config("interfaces")
|
||||
if interfaces do
|
||||
for {iface, _} <- interfaces do
|
||||
stop_interface(netman, iface)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_set_time do
|
||||
{:ok, ntp} = get_config("ntp")
|
||||
if ntp do
|
||||
Logger.info ">> starting ntp client."
|
||||
Ntp.set_time
|
||||
end
|
||||
:ok
|
||||
end
|
||||
|
||||
if Mix.env() == :prod do
|
||||
|
||||
def read_ssh_public_key do
|
||||
{:ok, public_key} = get_config("ssh")
|
||||
public_key
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
results = case File.read("#{System.get_env("HOME")}/.ssh/id_rsa.pub") do
|
||||
{:ok, bin} -> String.trim(bin)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
def read_ssh_public_key, do: unquote(results)
|
||||
end
|
||||
|
||||
defp maybe_start_ssh do
|
||||
public_key = read_ssh_public_key()
|
||||
ssh_dir = '/ssh'
|
||||
ssh_dir_exists? = File.exists?(ssh_dir)
|
||||
shell = {Elixir.IEx, :start, []}
|
||||
# shell = {Elixir.Nerves.Runtime.Shell, :start, []}
|
||||
if is_binary(public_key) and ssh_dir_exists? do
|
||||
File.write "#{ssh_dir}/authorized_keys", public_key
|
||||
debug_log "Starting SSH."
|
||||
:ok = :ssh.start()
|
||||
opts =
|
||||
[
|
||||
{:shell, shell},
|
||||
{:system_dir, ssh_dir},
|
||||
{:user_dir, ssh_dir},
|
||||
]
|
||||
:ssh.daemon(22, opts)
|
||||
:ok
|
||||
else
|
||||
debug_log "Not starting SSH"
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_get_fpf(%Context{} = ctx) do
|
||||
# First Party Farmware is not really a network concern but here we are...
|
||||
call = {:get, Configuration, "first_party_farmware"}
|
||||
{:ok, fpf} = GenServer.call(CS, call)
|
||||
|
||||
try do
|
||||
if fpf do
|
||||
Logger.info ">> is installing first party Farmwares.", type: :busy
|
||||
repo = Farmbot.Farmware.Installer.Repository.Farmbot
|
||||
Farmbot.Farmware.Installer.enable_repo!(ctx, repo)
|
||||
end
|
||||
:ok
|
||||
rescue
|
||||
error ->
|
||||
Logger.warn(">> failed to install first party farmwares: " <>
|
||||
" #{inspect error}")
|
||||
debug_log "#{inspect System.stacktrace()}"
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp get_context(netman) when is_atom(netman) or is_pid(netman) do
|
||||
GenServer.call(netman, :get_context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks for nxdomain. reboots if `nxdomain`.
|
||||
"""
|
||||
def connection_test(%Context{} = ctx) do
|
||||
Logger.info ">> doing connection test...", type: :busy
|
||||
case HTTP.get(ctx, "http://neverssl.com/") do
|
||||
{:ok, _} ->
|
||||
Logger.info ">> connection test complete", type: :success
|
||||
:ok
|
||||
{:error, :nxdomain} ->
|
||||
debug_log "escaping from nxdomain."
|
||||
Farmbot.System.reboot
|
||||
{:error, reason} ->
|
||||
debug_log "not escaping."
|
||||
Farmbot.System.factory_reset("Fatal Error during "
|
||||
<> "connection test! #{inspect reason}")
|
||||
error ->
|
||||
Farmbot.System.factory_reset("Fatal Error during "
|
||||
<> "connection test! #{inspect error}")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Connected to the World Wide Web. Should be called from the
|
||||
callback module.
|
||||
"""
|
||||
def on_connect(context, pre_fun \\ nil, post_fun \\ nil)
|
||||
def on_connect(%Context{} = context, pre_fun, post_fun) do
|
||||
try do
|
||||
# If we were supplied a pre connect callback, do that.
|
||||
if pre_fun, do: pre_fun.()
|
||||
|
||||
:ok = connection_test(context)
|
||||
|
||||
:ok = maybe_set_time()
|
||||
:ok = maybe_start_ssh()
|
||||
:ok = maybe_get_fpf(context)
|
||||
|
||||
Logger.info ">> is trying to log in."
|
||||
{:ok, token} = Auth.try_log_in!(context.auth)
|
||||
|
||||
:ok = maybe_setup_rollbar(token)
|
||||
|
||||
if post_fun do
|
||||
post_fun.(token)
|
||||
end
|
||||
|
||||
{:ok, token}
|
||||
rescue
|
||||
exception ->
|
||||
Logger.error "Farmbot failed to log in."
|
||||
debug_log("#{inspect exception}")
|
||||
Farmbot.System.factory_reset("#{inspect exception}")
|
||||
end
|
||||
end
|
||||
|
||||
if Mix.env == :prod do
|
||||
|
||||
# Only set these attributes if we are in prod pls
|
||||
@access_token Application.get_env(:farmbot, :rollbar_access_token)
|
||||
@commit Mix.Project.config[:commit]
|
||||
@target Mix.Project.config[:target]
|
||||
|
||||
def maybe_setup_rollbar(token) do
|
||||
if String.contains?(token.unencoded.iss, "farmbot.io") and @access_token do
|
||||
Logger.info ">> Setting up rollbar!"
|
||||
# Application.put_env(:rollbax, :access_token, @access_token)
|
||||
# Application.put_env(:rollbax, :environment, token.unencoded.iss)
|
||||
# Application.put_env(:rollbax, :enabled, true)
|
||||
# Application.put_env(:rollbas, :custom, %{target: @target})
|
||||
#
|
||||
# Application.put_env(:farmbot, :rollbar_occurrence_data, %{
|
||||
# person_id: token.unencoded.bot,
|
||||
# person_email: token.unencoded.sub,
|
||||
# person_username: token.unencoded.bot,
|
||||
# framework: "Nerves",
|
||||
# code_version: @commit,
|
||||
# })
|
||||
# Application.ensure_all_started(:rollbax)
|
||||
|
||||
:ok = ExRollbar.setup [
|
||||
person_username: token.unencoded.bot,
|
||||
# enable_logger: true,
|
||||
person_email: token.unencoded.sub,
|
||||
code_version: @commit,
|
||||
access_token: @access_token,
|
||||
environment: token.unencoded.iss,
|
||||
person_id: token.unencoded.bot,
|
||||
framework: "Nerves",
|
||||
custom: %{target: @target}
|
||||
]
|
||||
:ok
|
||||
else
|
||||
Logger.info ">> Not Setting up rollbar!"
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
def maybe_setup_rollbar(_) do
|
||||
Logger.info ">> Not Setting up rollbar!"
|
||||
Logger.info ">> Setting up slack updater"
|
||||
:ok
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@spec get_config(binary) :: {:ok, any}
|
||||
defp get_config(key), do: GenServer.call(CS, {:get, Network, key})
|
||||
# @spec get_config() :: {:ok, false | map}
|
||||
# defp get_config, do: GenServer.call(CS, {:get, Network, :all})
|
||||
|
||||
defp get_mod(netman), do: GenServer.call(netman, :get_mod)
|
||||
|
||||
# GENSERVER STUFF
|
||||
|
||||
def handle_call(:get_context, _, state), do: {:reply, state.context, state}
|
||||
|
||||
def handle_call(:get_mod, _, state), do: {:reply, state.target, state}
|
||||
def handle_call({:scan, interface_name}, _, state) do
|
||||
f = mod(state.target).scan(state.context, interface_name)
|
||||
{:reply, f, state}
|
||||
end
|
||||
|
||||
def handle_call(:enumerate, _, state) do
|
||||
f = mod(state.target).enumerate(state.context)
|
||||
{:reply, f, state}
|
||||
end
|
||||
|
||||
def handle_call({:start, interface, settings}, _, state) do
|
||||
f = mod(state.target).start_interface(state.context, interface, settings)
|
||||
{:reply, f, state}
|
||||
end
|
||||
|
||||
def handle_call({:stop, interface}, _, state) do
|
||||
f = mod(state.target).stop_interface(state.context, interface)
|
||||
{:reply, f, state}
|
||||
end
|
||||
|
||||
def terminate(reason, state) do
|
||||
target_pid = Process.whereis(mod(state.target))
|
||||
if target_pid do
|
||||
GenServer.stop(target_pid, reason)
|
||||
end
|
||||
end
|
||||
|
||||
# Behavior
|
||||
@type return_type :: :ok | {:error, term}
|
||||
@callback scan(Context.t, binary) :: [binary] | {:error, term}
|
||||
@callback enumerate(Context.t) :: [binary] | {:error, term}
|
||||
@callback start_interface(Context.t, binary, map) :: return_type
|
||||
@callback stop_interface(Context.t, binary) :: return_type
|
||||
@callback start_link(Context.t) :: {:ok, pid}
|
||||
end
|
|
@ -1,87 +0,0 @@
|
|||
defmodule Redis.Client.Public do
|
||||
@moduledoc """
|
||||
Public api for interfacing redis.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Sends a command to redis. Blocks
|
||||
"""
|
||||
@spec send_redis(pid, [binary]) :: binary
|
||||
def send_redis(conn, stuff), do: Redix.command!(conn, stuff)
|
||||
|
||||
@doc """
|
||||
Input a value by a given key.
|
||||
"""
|
||||
@spec input_value(pid, String.t, any) :: [String.t]
|
||||
def input_value(redis, key, value) when is_map(value) do
|
||||
input_map(redis, %{key => value})
|
||||
end
|
||||
|
||||
def input_value(redis, key, value) when is_list(value) do
|
||||
input_list(redis, key, value)
|
||||
end
|
||||
|
||||
def input_value(redis, key, value) when is_tuple(value) do
|
||||
input_value(redis, key, Tuple.to_list(value))
|
||||
end
|
||||
|
||||
def input_value(redis, key, value),
|
||||
do: send_redis redis, ["SET", key, value]
|
||||
|
||||
defp input_list(redis, key, list) do
|
||||
send_redis redis, ["DEL", key]
|
||||
for i <- list do
|
||||
if is_binary(i) or is_integer(i) do
|
||||
send_redis redis, ["RPUSH", key, i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec input_map(pid, map | struct, String.t | nil) :: [String.t]
|
||||
defp input_map(redis, map, bloop \\ nil)
|
||||
defp input_map(redis, %{__struct__: _} = map, bloop),
|
||||
do: input_map(redis, map |> Map.from_struct, bloop)
|
||||
|
||||
defp input_map(redis, map, bloop) when is_map(map) do
|
||||
f = Enum.map(map, fn({key, value}) ->
|
||||
cond do
|
||||
# When value is a map, we need to go deeper.
|
||||
is_map(value) ->
|
||||
handle_map_value(bloop, redis, key, value)
|
||||
|
||||
# When its a map we have to do some magic.
|
||||
is_list(value) ->
|
||||
handle_list_value(bloop, redis, key, value)
|
||||
|
||||
# if neither of those
|
||||
true ->
|
||||
handle_rest_value(bloop, redis, key, value)
|
||||
end
|
||||
end)
|
||||
List.flatten(f)
|
||||
end
|
||||
|
||||
defp handle_map_value(bloop, redis, key, value) do
|
||||
if bloop do
|
||||
input_map(redis, value, "#{bloop}.#{key}")
|
||||
else
|
||||
input_map(redis, value, key)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_list_value(bloop, redis, key, value) do
|
||||
if bloop do
|
||||
input_list(redis, "#{bloop}.#{key}", value)
|
||||
else
|
||||
input_list(redis, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_rest_value(bloop, redis, key, value) do
|
||||
if bloop do
|
||||
input_value(redis, "#{bloop}.#{key}", value)
|
||||
else
|
||||
input_value(redis, key, value)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,99 +0,0 @@
|
|||
defmodule Redis.Server do
|
||||
@moduledoc """
|
||||
Port for a redis server
|
||||
"""
|
||||
@config Application.get_all_env(:farmbot)[:redis]
|
||||
|
||||
if Mix.env() == :dev do
|
||||
def should_bind, do: "bind 0.0.0.0"
|
||||
else
|
||||
def should_bind, do: "bind 127.0.0.1"
|
||||
end
|
||||
|
||||
def config(path: dir), do: ~s"""
|
||||
#{should_bind()}
|
||||
protected-mode yes
|
||||
port #{@config[:port]}
|
||||
tcp-backlog 511
|
||||
unixsocket /tmp/redis.sock
|
||||
unixsocketperm 700
|
||||
timeout 0
|
||||
tcp-keepalive 0
|
||||
supervised no
|
||||
pidfile /var/run/redis_6379.pid
|
||||
loglevel notice
|
||||
logfile ""
|
||||
databases 1
|
||||
stop-writes-on-bgsave-error yes
|
||||
rdbcompression yes
|
||||
rdbchecksum yes
|
||||
# dbfilename /tmp/dump.rdb
|
||||
dir #{dir}
|
||||
slave-serve-stale-data yes
|
||||
slave-read-only yes
|
||||
repl-diskless-sync no
|
||||
repl-diskless-sync-delay 5
|
||||
repl-disable-tcp-nodelay no
|
||||
slave-priority 100
|
||||
appendonly no
|
||||
appendfilename "appendonly.aof"
|
||||
no-appendfsync-on-rewrite no
|
||||
auto-aof-rewrite-percentage 100
|
||||
auto-aof-rewrite-min-size 64mb
|
||||
hash-max-ziplist-value 64
|
||||
zset-max-ziplist-value 64
|
||||
client-output-buffer-limit slave 256mb 64mb 60
|
||||
client-output-buffer-limit pubsub 32mb 8mb 60
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
use Farmbot.DebugLog
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Start the redis server.
|
||||
"""
|
||||
def start_link(_, opts), do: GenServer.start_link(__MODULE__, [], opts)
|
||||
|
||||
def config_file do
|
||||
File.write("/tmp/redis.config", config(path: Farmbot.System.FS.path))
|
||||
"/tmp/redis.config"
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
kill_redis()
|
||||
exe = System.find_executable("redis-server")
|
||||
port = Port.open({:spawn_executable, exe},
|
||||
[:stream,
|
||||
:binary,
|
||||
:exit_status,
|
||||
:hide,
|
||||
:use_stdio,
|
||||
:stderr_to_stdout,
|
||||
args: ['#{config_file()}']])
|
||||
# Port.connect(port, self())
|
||||
{:ok, port}
|
||||
end
|
||||
|
||||
def handle_info({_port, {:data, info}}, port) do
|
||||
debug_log info
|
||||
{:noreply, port}
|
||||
end
|
||||
|
||||
def handle_info({_port, _info}, port) do
|
||||
{:noreply, port}
|
||||
end
|
||||
|
||||
def kill_redis do
|
||||
Logger.info "trying to kill old redis"
|
||||
case System.cmd("killall", ["redis-server"]) do
|
||||
{_, 0} ->
|
||||
Process.sleep(5000)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def terminate(_, _port) do
|
||||
kill_redis()
|
||||
end
|
||||
end
|
|
@ -1,44 +1,26 @@
|
|||
defmodule Farmbot.System.Supervisor do
|
||||
@moduledoc """
|
||||
Supervises Platform specific stuff for Farmbot to operate
|
||||
Supervises Platform specific stuff for Farmbot to operate
|
||||
"""
|
||||
use Supervisor
|
||||
@redis_config Application.get_all_env(:farmbot)[:redis]
|
||||
@target Mix.Project.config()[:target]
|
||||
alias Farmbot.Context
|
||||
use Supervisor
|
||||
import Farmbot.System.Init
|
||||
|
||||
alias Farmbot.System.Network
|
||||
error_msg = """
|
||||
Please configure your environment's init system!
|
||||
"""
|
||||
|
||||
def start_link(%Context{} = context, opts) do
|
||||
Supervisor.start_link(__MODULE__, context, opts)
|
||||
@children Application.get_env(:farmbot, :init) || Mix.raise(error_msg)
|
||||
|
||||
@doc "Start the System Services. This is more or less `init`."
|
||||
def start_link(args, opts \\ []) do
|
||||
Supervisor.start_link(__MODULE__, args, opts)
|
||||
end
|
||||
|
||||
def init(context) do
|
||||
children = [
|
||||
worker(Farmbot.System.FS,
|
||||
[context, @target, [name: Farmbot.System.FS]]),
|
||||
|
||||
worker(Farmbot.System.FS.Worker,
|
||||
[context, @target, [name: Farmbot.System.FS.Worker]]),
|
||||
|
||||
worker(Farmbot.System.FS.ConfigStorage,
|
||||
[context, [name: Farmbot.System.FS.ConfigStorage]]),
|
||||
|
||||
worker(Network,
|
||||
[context, @target, [name: Network]]),
|
||||
worker(Farmbot.FactoryResetWatcher, [context, Network, []])
|
||||
]
|
||||
++ maybe_redis(context)
|
||||
opts = [strategy: :one_for_one]
|
||||
def init(args) do
|
||||
children = Enum.map(@children, fn(child) ->
|
||||
fb_init(child, [args, [name: child]])
|
||||
end)
|
||||
opts = [strategy: :one_for_all]
|
||||
supervise(children, opts)
|
||||
end
|
||||
|
||||
@spec maybe_redis(Context.t) :: [Supervisor.Spec.spec]
|
||||
defp maybe_redis(context) do
|
||||
if @redis_config[:server] do
|
||||
[worker(Redis.Server, [context, [name: Redis.Server]])]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "host", FileSystem]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System.FS
|
||||
|
||||
def fs_init do
|
||||
path = Application.get_env(:farmbot, :path)
|
||||
File.mkdir_p!(path)
|
||||
:ok
|
||||
end
|
||||
|
||||
def mount_read_only, do: :ok
|
||||
def mount_read_write, do: :ok
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "host", Network]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System.Network
|
||||
use GenServer
|
||||
|
||||
def init(_) do
|
||||
{:ok, []}
|
||||
end
|
||||
def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
def scan(_, _), do: ["testssod", "test2"]
|
||||
def enumerate(_), do: ["testiface0"]
|
||||
def start_interface(_, _, _), do: :ok
|
||||
def stop_interface(_, _), do: :ok
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "host"]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System
|
||||
|
||||
def reboot, do: :ok
|
||||
def power_off, do: :ok
|
||||
|
||||
def factory_reset(reason) do
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
File.rm_rf! "#{path()}"
|
||||
File.mkdir_p! "#{path()}"
|
||||
File.write("#{path()}/factory_reset_reason", reason)
|
||||
end, true
|
||||
System.halt(0)
|
||||
end
|
||||
|
||||
defp path, do: Farmbot.System.FS.path()
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "host", FileSystem]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System.FS
|
||||
|
||||
def fs_init do
|
||||
path = Application.get_env(:farmbot, :path)
|
||||
File.mkdir_p!(path)
|
||||
:ok
|
||||
end
|
||||
|
||||
def mount_read_only, do: :ok
|
||||
def mount_read_write, do: :ok
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "host", Network]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System.Network
|
||||
use GenServer
|
||||
|
||||
def init(_) do
|
||||
{:ok, []}
|
||||
end
|
||||
def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
def scan(_, _), do: ["testssod", "test2"]
|
||||
def enumerate(_), do: ["testiface0"]
|
||||
def start_interface(_, _, _), do: :ok
|
||||
def stop_interface(_, _), do: :ok
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "host"]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System
|
||||
|
||||
def reboot, do: :ok
|
||||
def power_off, do: :ok
|
||||
|
||||
def factory_reset(reason) do
|
||||
files = path() |> File.ls!()
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
File.rm_rf files
|
||||
File.write("#{path()}/factory_reset_reason", reason)
|
||||
end, true
|
||||
:ok
|
||||
end
|
||||
|
||||
defp path, do: Farmbot.System.FS.path()
|
||||
end
|
|
@ -1,87 +0,0 @@
|
|||
defmodule Farmbot.System.NervesCommon.FileSystem do
|
||||
@moduledoc """
|
||||
Common filesystem functionality for Nerves Devices
|
||||
"""
|
||||
|
||||
defmacro __using__(
|
||||
target: _,
|
||||
ro_options: ro_options,
|
||||
rw_options: rw_options,
|
||||
state_path: state_path,
|
||||
fs_type: fs_type,
|
||||
block_device: block_device)
|
||||
do
|
||||
quote do
|
||||
require Logger
|
||||
# mount -t ext4 -o ro,remount /dev/mmcblk0p3 /state
|
||||
|
||||
@doc false
|
||||
def mount_read_only,
|
||||
do: "mount" |> System.cmd(unquote(ro_options)) |> parse_cmd
|
||||
@doc false
|
||||
def mount_read_write,
|
||||
do: "mount" |> System.cmd(unquote(rw_options)) |> parse_cmd
|
||||
|
||||
@doc false
|
||||
def fs_init do
|
||||
# check if the formatted flag exists
|
||||
with {:error, _} <- File.read("#{unquote(state_path)}/.formatted") do
|
||||
# If it does, format the state partition
|
||||
:ok = format_state_part()
|
||||
else
|
||||
# If not, we are fine. continue
|
||||
_ ->
|
||||
File.touch "/tmp/authorized_keys"
|
||||
:ok
|
||||
end
|
||||
|
||||
:ok = tzdata_hack()
|
||||
end
|
||||
|
||||
@doc """
|
||||
This needs to happen because tzdata by default polls and downloads
|
||||
things to its release dir, which is read only in nerves environments.
|
||||
to fix this we bundle the file it would normally download, and package
|
||||
it into farmbot, then copy it to a configured dir for tzdata to
|
||||
use because /tmp is read-write, we will just copy it there at every
|
||||
boot becase its easier.
|
||||
"""
|
||||
def tzdata_hack do
|
||||
Logger.info ">> Hacking tzdata..."
|
||||
# File.cp "#{:code.priv_dir(:tzdata)}/release_ets/2016c.ets", "/tmp/"
|
||||
File.write "/tmp/latest_remote_poll.txt", "2017-2-14"
|
||||
File.mkdir "/tmp/release_ets"
|
||||
File.cp_r "#{:code.priv_dir(:farmbot)}/release_ets/2016j.ets", "/tmp/release_ets/2016j.ets"
|
||||
Logger.info ">> Hacked!"
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def factory_reset do
|
||||
:ok = format_state_part()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp parse_cmd({_, 0}), do: :ok
|
||||
defp parse_cmd({err, num}),
|
||||
do: raise "error doing command(#{num}): #{inspect err}"
|
||||
|
||||
defp format_state_part do
|
||||
state_path = unquote(state_path)
|
||||
# Format partition
|
||||
System.cmd("mkfs.#{unquote(fs_type)}", ["#{unquote(block_device)}", "-F"])
|
||||
# Mount it as read/write
|
||||
# NOTE(connor): is there a reason i did this in band?
|
||||
System.cmd("mount", ["-t", unquote(fs_type), "-o", "rw",
|
||||
unquote(block_device), state_path])
|
||||
# Basically a flag that says the partition is formatted.
|
||||
File.write!("#{unquote(state_path)}/.formatted", "DONT CAT ME\n")
|
||||
|
||||
File.mkdir_p! "#{state_path}/farmware"
|
||||
File.mkdir_p! "#{state_path}/farmware/packages"
|
||||
File.mkdir_p! "#{state_path}/farmware/repos"
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,293 +0,0 @@
|
|||
defmodule Farmbot.System.NervesCommon.Network do
|
||||
@doc """
|
||||
Common network functionality for Nerves Devices.
|
||||
"""
|
||||
|
||||
defmacro __using__(opts) do
|
||||
modules = Keyword.get(opts, :modules, [])
|
||||
callback = Keyword.get(opts, :callback)
|
||||
target = Keyword.get(opts, :target)
|
||||
quote do
|
||||
@behaviour Farmbot.System.Network
|
||||
use GenServer
|
||||
require Logger
|
||||
alias Nerves.InterimWiFi, as: NervesWifi
|
||||
alias Nerves.{NetworkInterface, Udhcpc, WpaSupplicant, SSDPServer}
|
||||
alias Farmbot.System.Network.Hostapd
|
||||
alias Farmbot.Context
|
||||
use Farmbot.DebugLog
|
||||
|
||||
def start_link(%Context{} = ctx),
|
||||
do: GenServer.start_link(__MODULE__, ctx, name: __MODULE__)
|
||||
|
||||
def init(context) do
|
||||
for module <- unquote(modules) do
|
||||
{_, 0} = System.cmd("modprobe", [module])
|
||||
end
|
||||
|
||||
# wait for a few seconds for everything to settle
|
||||
Process.sleep(5000)
|
||||
|
||||
# execute a callback if supplied.
|
||||
cb = unquote(callback)
|
||||
if cb do
|
||||
Logger.info ">> doing target specific setup: #{unquote(target)}"
|
||||
cb.()
|
||||
end
|
||||
|
||||
{:ok, %{logging_in: false, context: context, retries: 0}}
|
||||
end
|
||||
|
||||
# don't start this interface
|
||||
def start_interface(%Context{} = ctx, _interface, %{"default" => false}) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def start_interface(%Context{} = ctx, interface, %{"default" => "dhcp", "type" => "wired"} = s) do
|
||||
interface |> NervesWifi.setup([]) |> cast_start_iface(interface, s)
|
||||
end
|
||||
|
||||
def start_interface(%Context{} = ctx, interface,
|
||||
%{"default" => "dhcp",
|
||||
"type" => "wireless",
|
||||
"settings" => settings} = s)
|
||||
do
|
||||
ssid = settings["ssid"]
|
||||
case settings["key_mgmt"] do
|
||||
"NONE" ->
|
||||
interface
|
||||
|> NervesWifi.setup([ssid: ssid, key_mgmt: :NONE])
|
||||
|> cast_start_iface(interface, s)
|
||||
"WPA-PSK" ->
|
||||
psk = settings["psk"]
|
||||
interface
|
||||
|> NervesWifi.setup([ssid: ssid, key_mgmt: :"WPA-PSK", psk: psk])
|
||||
|> cast_start_iface(interface, s)
|
||||
end
|
||||
:ok
|
||||
end
|
||||
|
||||
def start_interface(%Context{} = ctx, interface,
|
||||
%{"default" => "hostapd",
|
||||
"settings" => %{"ipv4_address" => ip_addr}, "type" => "wireless"} = s)
|
||||
do
|
||||
{:ok, manager} = GenEvent.start_link()
|
||||
[interface: interface, ip_address: ip_addr, manager: manager]
|
||||
|> Hostapd.start_link()
|
||||
|> cast_start_iface(interface, s)
|
||||
end
|
||||
|
||||
def cast_start_iface(blah, interface, settings) do
|
||||
case blah do
|
||||
{:ok, pid} ->
|
||||
GenServer.cast(__MODULE__,
|
||||
{:start_interface, interface, settings, pid})
|
||||
:ok
|
||||
{:error, :already_added} ->
|
||||
:ok
|
||||
{:error, reason} ->
|
||||
Logger.error("Encountered an error starting " <>
|
||||
"#{interface}: #{reason}")
|
||||
{:error, reason}
|
||||
error ->
|
||||
Logger.error("Encountered an error starting " <>
|
||||
"#{interface}: #{error}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def stop_interface(%Context{} = _ctx, interface) do
|
||||
GenServer.call(__MODULE__, {:stop_interface, interface})
|
||||
end
|
||||
|
||||
def scan(%Context{} = ctx, iface), do: do_scan(iface)
|
||||
|
||||
def do_scan(iface, retry \\ false)
|
||||
|
||||
def do_scan(iface, true) do
|
||||
{:ok, _} = NervesWifi.setup(iface)
|
||||
Process.sleep(1000)
|
||||
results =
|
||||
case do_iw_scan(iface) do
|
||||
{:ok, f} -> f
|
||||
{:error, _} ->
|
||||
Logger.error ">> Could not scan on #{iface}. " <>
|
||||
"The device either isn't wireless or uses the legacy WEXT driver."
|
||||
[]
|
||||
end
|
||||
NervesWifi.teardown(iface)
|
||||
results
|
||||
end
|
||||
|
||||
def do_scan(iface, false) do
|
||||
case do_iw_scan(iface) do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> do_scan(iface, true)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_iw_scan(iface) do
|
||||
case System.cmd("iw", [iface, "scan", "ap-force"]) do
|
||||
{res, 0} ->
|
||||
f = res |> clean_ssid
|
||||
{:ok, f}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate(%Context{} = _ctx), do: NetworkInterface.interfaces -- ["lo"]
|
||||
|
||||
defp clean_ssid(hc) do
|
||||
hc
|
||||
|> String.replace("\t", "")
|
||||
|> String.replace("\\x00", "")
|
||||
|> String.split("\n")
|
||||
|> Enum.filter(fn(s) -> String.contains?(s, "SSID: ") end)
|
||||
|> Enum.map(fn(z) -> String.replace(z, "SSID: ", "") end)
|
||||
|> Enum.filter(fn(z) -> String.length(z) != 0 end)
|
||||
end
|
||||
|
||||
# GENSERVER STUFF
|
||||
|
||||
def handle_call(:logged_in, _from, state) do
|
||||
{:reply, :ok, %{state | logging_in: false}}
|
||||
end
|
||||
|
||||
def handle_call({:stop_interface, interface}, _, state) do
|
||||
case state[interface] do
|
||||
{settings, pid} ->
|
||||
if settings["default"] == "hostapd" do
|
||||
if Process.alive?(pid) do
|
||||
GenServer.stop(pid, :normal)
|
||||
end
|
||||
{:reply, :ok, Map.delete(state, interface)}
|
||||
else
|
||||
:ok = Registry.unregister(NetworkInterface, interface)
|
||||
:ok = Registry.unregister(Udhcpc, interface)
|
||||
:ok = Registry.unregister(WpaSupplicant, interface)
|
||||
Logger.warn ">> cant stop: #{interface}"
|
||||
{:reply, {:error, :not_implemented}, state}
|
||||
end
|
||||
_ -> {:reply, {:error, :not_started}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:start_interface, interface, settings, pid}, state) do
|
||||
{:ok, _} = Registry.register(NetworkInterface, interface, [])
|
||||
{:ok, _} = Registry.register(Udhcpc, interface, [])
|
||||
{:ok, _} = Registry.register(WpaSupplicant, interface, [])
|
||||
{:noreply, Map.put(state, interface, {settings, pid})}
|
||||
end
|
||||
|
||||
def handle_info({:ssdp_timer, ip, uuid} = msg, state) do
|
||||
spawn fn() ->
|
||||
fields = ssdp_fields(ip)
|
||||
{:ok, _} = SSDPServer.publish "uuid:#{uuid}", "nerves:farmbot", fields
|
||||
end
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{Udhcpc, :bound, %{ifname: interface, ipv4_address: ip}},
|
||||
%{logging_in: true} = state
|
||||
) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{Udhcpc, :bound, %{ifname: interface, ipv4_address: ip}},
|
||||
state
|
||||
) do
|
||||
that = self()
|
||||
spawn fn() ->
|
||||
Farmbot.System.Network.on_connect(state.context,
|
||||
# before callback
|
||||
fn() ->
|
||||
try do
|
||||
{_, 0} = System.cmd("epmd", ["-daemon"])
|
||||
:net_kernel.start(['farmbot@#{ip}'])
|
||||
rescue
|
||||
_ ->
|
||||
debug_log "could not start epmd or net_kernel"
|
||||
:ok
|
||||
end
|
||||
Logger.info ">> is waiting for linux and network and what not."
|
||||
Process.sleep(5000) # ye old race linux condidtion
|
||||
uuid = UUID.uuid1()
|
||||
send(that, {:ssdp_timer, ip, uuid})
|
||||
GenServer.call(that, :logged_in)
|
||||
end,
|
||||
|
||||
# after callback
|
||||
fn(token) ->
|
||||
for {key, value} <- state do
|
||||
if match?({%{"default" => "hostapd"}, _}, value) do
|
||||
Logger.info "Killing #{key}"
|
||||
stop_interface(state.context, key)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
{:noreply, %{state | logging_in: true}}
|
||||
end
|
||||
|
||||
def handle_info({WpaSupplicant, {:error, :psk, :FAIL}, %{ifname: _iface}}, state) do
|
||||
Farmbot.System.factory_reset("""
|
||||
I could not authenticate with the access point. This could be a bad
|
||||
password, or an unsupported network type.
|
||||
""")
|
||||
{:stop, :factory_reset, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{WpaSupplicant, _event, %{ifname: _iface}},
|
||||
%{retries: retries} = state
|
||||
) when retries > 5 do
|
||||
Farmbot.System.factory_reset("""
|
||||
I could not find the wifi access point. Check that it was inputted correctly.
|
||||
I tried #{retries} times and still found nothing. Maybe I'm not close enough to the access point?
|
||||
""")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{WpaSupplicant, event, %{ifname: _iface}},
|
||||
state
|
||||
) when is_atom(event) do
|
||||
event = event |> Atom.to_string
|
||||
wrong_key? = event |> String.contains?("reason=WRONG_KEY")
|
||||
not_found? = event |> String.contains?("CTRL-EVENT-NETWORK-NOT-FOUND")
|
||||
if wrong_key?, do: Farmbot.System.factory_reset("""
|
||||
I could not authenticate with the access point. This could be a bad
|
||||
password, or an unsupported network type.
|
||||
""")
|
||||
|
||||
if not_found? do
|
||||
{:noreply, %{state | retries: state.retries + 1}}
|
||||
else
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(info, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def terminate(_, _state) do
|
||||
# TODO STOP INTERFACES
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ssdp_fields(ip) do
|
||||
[
|
||||
location: "http://#{ip}/ssdp",
|
||||
server: "Farmbot",
|
||||
node: node(),
|
||||
"cache-control": "max-age=1800"
|
||||
]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,24 +0,0 @@
|
|||
defmodule Farmbot.System.NervesCommon do
|
||||
@moduledoc """
|
||||
Common functionality for nerves devices.
|
||||
"""
|
||||
defmacro __using__(target: _) do
|
||||
quote do
|
||||
def reboot, do: Nerves.Firmware.reboot()
|
||||
|
||||
def power_off, do: Nerves.Firmware.poweroff()
|
||||
|
||||
def factory_reset(reason) do
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
File.rm_rf "#{path()}/config.json"
|
||||
File.rm_rf "#{path()}/secret"
|
||||
File.rm_rf "#{path()}/farmware"
|
||||
File.write("#{path()}/factory_reset_reason", reason)
|
||||
end, true
|
||||
reboot()
|
||||
end
|
||||
|
||||
defp path, do: Farmbot.System.FS.path()
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,63 +0,0 @@
|
|||
defmodule Farmbot.System.NervesCommon.Updates do
|
||||
defmacro __using__(target: _) do
|
||||
quote do
|
||||
@behaviour Farmbot.System.Updates
|
||||
require Logger
|
||||
|
||||
def install(path), do: :ok = Nerves.Firmware.upgrade_and_finalize(path)
|
||||
|
||||
defp blerp(tries \\ 0)
|
||||
|
||||
defp blerp(tries) when tries > 10 do
|
||||
Logger.info "No serial handler", type: :warn
|
||||
:ok
|
||||
end
|
||||
|
||||
defp blerp(tries) do
|
||||
ctx = Farmbot.Context.new()
|
||||
pid = Process.whereis(ctx.serial)
|
||||
if is_pid(pid) do
|
||||
r = Farmbot.Serial.Handler.write ctx, "F83"
|
||||
if r == :timeout do
|
||||
Logger.info "Got timeout waiting for serial handler. Trying again"
|
||||
blerp(tries + 1)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
else
|
||||
Logger.info "No serial handler yet, waiting...", type: :warn
|
||||
Process.sleep(5000)
|
||||
blerp(tries + 1)
|
||||
end
|
||||
end
|
||||
|
||||
def post_install do
|
||||
Logger.info ">> Is doing post install stuff."
|
||||
:ok = blerp()
|
||||
ctx = Farmbot.Context.new
|
||||
r = Farmbot.Serial.Handler.write ctx, "F83"
|
||||
exp = Application.get_all_env(:farmbot)[:expected_fw_version]
|
||||
case r do
|
||||
{:report_software_version, version} when version == exp ->
|
||||
Logger.info "Firmware is already the correct version!"
|
||||
:ok
|
||||
other ->
|
||||
# we need to flash the firmware
|
||||
IO.warn "#{inspect other}"
|
||||
file = "#{:code.priv_dir(:farmbot)}/firmware.hex"
|
||||
Logger.info ">> Doing post update firmware flash.", type: :warn
|
||||
GenServer.cast(ctx.serial, {:update_fw, file, self()})
|
||||
wait_for_finish()
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_for_finish do
|
||||
receive do
|
||||
:done -> :ok
|
||||
_e -> :reboot
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "rpi3", FileSystem]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System.FS
|
||||
@state_path Application.get_env(:farmbot, :path)
|
||||
@block_device "/dev/mmcblk0p3"
|
||||
@fs_type "ext4"
|
||||
@ro_options ["-t", @fs_type, "-o", "ro,remount", @block_device, @state_path]
|
||||
@rw_options ["-t", @fs_type, "-o", "rw,remount", @block_device, @state_path]
|
||||
use Farmbot.System.NervesCommon.FileSystem,
|
||||
target: "rpi3",
|
||||
ro_options: @ro_options,
|
||||
rw_options: @rw_options,
|
||||
state_path: @state_path,
|
||||
fs_type: @fs_type,
|
||||
block_device: @block_device
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "rpi3", Network]) do
|
||||
use Farmbot.System.NervesCommon.Network,
|
||||
target: "rpi3", modules: ["brcmfmac"], callback: fn() ->
|
||||
# iw dev wlan0 interface add wlan1 type __ap
|
||||
# iw_args = ~w"dev wlan0 interface add wlan1 type __ap"
|
||||
# {_, 0} = System.cmd("iw", iw_args)
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -1,6 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "rpi3"]) do
|
||||
@moduledoc false
|
||||
@behaviour Farmbot.System
|
||||
|
||||
use Farmbot.System.NervesCommon, target: "rpi3"
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule Module.concat([Farmbot, System, "rpi3", Updates]) do
|
||||
use Farmbot.System.NervesCommon.Updates, target: "rpi3"
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
target = Mix.Project.config[:target]
|
||||
unless target == "host" do
|
||||
nerves_common_path = "lib/farmbot/system/targets/nerves_common"
|
||||
nc_files = File.ls! nerves_common_path
|
||||
for file <- nc_files, do: Code.eval_file(file, nerves_common_path)
|
||||
end
|
||||
|
||||
path = case Mix.env() do
|
||||
:test ->
|
||||
"lib/farmbot/system/targets/#{target}_test"
|
||||
_ ->
|
||||
"lib/farmbot/system/targets/#{target}"
|
||||
end
|
||||
|
||||
files = File.ls! path
|
||||
for file <- files, do: Code.eval_file(file, path)
|
|
@ -1,128 +0,0 @@
|
|||
defmodule Farmbot.System.Updates do
|
||||
@moduledoc """
|
||||
Check, download, apply updates
|
||||
"""
|
||||
|
||||
@target Mix.Project.config()[:target]
|
||||
@path "/tmp/update.fw"
|
||||
require Logger
|
||||
alias Farmbot.System.FS
|
||||
alias Farmbot.{Context, HTTP}
|
||||
|
||||
# TODO(connor): THIS IS A MINOR IMPROVEMENT FROM THE LAST VERSION OF THIS FILE
|
||||
# BUT IT DEFINATELY NEEDS FURTHER REFACTORING.
|
||||
|
||||
@spec mod(atom) :: atom
|
||||
defp mod(target), do: Module.concat([Farmbot, System, target, Updates])
|
||||
|
||||
defp releases_url(%Context{} = context) do
|
||||
{:ok, token} = Farmbot.Auth.get_token(context.auth)
|
||||
token.unencoded.os_update_server
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks for updates if the bot says so
|
||||
"""
|
||||
def do_update_check do
|
||||
context = Farmbot.Context.new()
|
||||
if Farmbot.BotState.get_config(context, :os_auto_update) do
|
||||
check_and_download_updates(context)
|
||||
else
|
||||
Logger.info ">> Will not do update check!"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks for updates, and if there is an update, downloads, and applies it.
|
||||
"""
|
||||
@spec check_and_download_updates(Context.t)
|
||||
:: :ok | {:error, term} | :no_updates
|
||||
def check_and_download_updates(%Context{} = ctx) do
|
||||
case check_updates(ctx) do
|
||||
{:update, url} ->
|
||||
Logger.info ">> has found a new Operating System update! #{url}",
|
||||
type: :busy
|
||||
install_updates(ctx, url)
|
||||
:no_updates ->
|
||||
Logger.info ">> is already on the latest Operating System version!",
|
||||
type: :success
|
||||
:no_updates
|
||||
{:error, reason} ->
|
||||
Logger.error ">> encountered an error checking for updates! #{reason}"
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks for updates
|
||||
"""
|
||||
@spec check_updates(Context.t)
|
||||
:: {:update, binary} | :no_updates | {:error, term}
|
||||
def check_updates(%Context{} = context) do
|
||||
current = Farmbot.BotState.get_os_version(context)
|
||||
if String.contains?(current, "rc") do
|
||||
msg = "Release Candidate Releases don't currently support updates!"
|
||||
Logger.info msg, type: :warn
|
||||
:no_updates
|
||||
else
|
||||
do_http_req(context)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_updates(Context.t)
|
||||
:: {:update, binary} | :no_updates | {:error, term}
|
||||
defp do_http_req(%Context{} = ctx) do
|
||||
case HTTP.get(ctx, releases_url(ctx)) do
|
||||
{:ok, %HTTP.Response{body: body, status_code: 200}} ->
|
||||
json = Poison.decode!(body)
|
||||
version = json["tag_name"]
|
||||
version_without_v = String.trim_leading version, "v"
|
||||
list = json["assets"]
|
||||
item = Enum.find(list, fn(item) ->
|
||||
item["name"] == "farmbot-#{@target}-#{version_without_v}.fw"
|
||||
end)
|
||||
url = item["browser_download_url"]
|
||||
{:update, url}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Installs an update from a url
|
||||
"""
|
||||
@spec install_updates(Context.t, String.t) :: no_return
|
||||
def install_updates(%Context{} = context, url) do
|
||||
# Ignore the compiler warning here.
|
||||
# "I'll fix it later i promise" -- Connor Rigby
|
||||
# "i promise I will fix this one day..." -- Connor Rigby
|
||||
fun = Farmbot.BotState.download_progress_fun(context, "FBOS_OTA")
|
||||
path = HTTP.download_file!(context, url, @path, fun)
|
||||
case File.stat(path) do
|
||||
{:ok, file} ->
|
||||
Logger.info "Found file: #{inspect file}", type: :success
|
||||
e -> Logger.error "Could not find update file: #{inspect e}"
|
||||
end
|
||||
|
||||
setup_post_update()
|
||||
|
||||
mod(@target).install(path)
|
||||
Farmbot.System.reboot()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Will cause `post_install/0` to be called next reboot.
|
||||
"""
|
||||
def setup_post_update do
|
||||
FS.transaction fn ->
|
||||
Logger.info "Seting up post update!", type: :busy
|
||||
path = "#{FS.path()}/.post_update"
|
||||
:ok = File.write(path, "DONT CAT ME\r\n")
|
||||
end, true
|
||||
end
|
||||
|
||||
@spec post_install :: no_return
|
||||
def post_install, do: mod(@target).post_install()
|
||||
|
||||
@callback install(binary) :: no_return
|
||||
@callback post_install() :: no_return
|
||||
end
|
|
@ -1,48 +0,0 @@
|
|||
defmodule Farmbot.Transport.Redis do
|
||||
@moduledoc """
|
||||
Interacts with teh redis db
|
||||
"""
|
||||
use GenStage
|
||||
require Logger
|
||||
alias Farmbot.Context
|
||||
@config Application.get_all_env(:farmbot)[:redis]
|
||||
@ping_time 5_000
|
||||
@save_time 900_000
|
||||
|
||||
@doc """
|
||||
Starts a stage for a Redis
|
||||
"""
|
||||
@spec start_link(Context.t, [{atom, any}]) :: {:ok, pid}
|
||||
def start_link(context, opts), do: GenStage.start_link(__MODULE__, context, opts)
|
||||
|
||||
def init(%Context{} = _context) do
|
||||
{:ok, conn} = Redix.start_link(host: "localhost", port: @config[:port])
|
||||
Process.link(conn)
|
||||
Process.send_after(self(), :ping, @ping_time)
|
||||
Process.send_after(self(), :save, @save_time)
|
||||
{:consumer, conn, subscribe_to: [Farmbot.Transport]}
|
||||
end
|
||||
|
||||
def handle_info({_from, {:status, stuff}}, redis) do
|
||||
Redis.Client.Public.input_value(redis, "BOT_STATUS", stuff)
|
||||
{:noreply, [], redis}
|
||||
end
|
||||
|
||||
def handle_info(:ping, conn) do
|
||||
Redis.Client.Public.send_redis(conn, ["PING"])
|
||||
Process.send_after(self(), :ping, @ping_time)
|
||||
{:noreply, [], conn}
|
||||
end
|
||||
|
||||
def handle_info(:save, conn) do
|
||||
Farmbot.System.FS.transaction fn() ->
|
||||
Redis.Client.Public.send_redis(conn, ["SAVE"])
|
||||
end
|
||||
Process.send_after(self(), :save, @save_time)
|
||||
{:noreply, [], conn}
|
||||
end
|
||||
|
||||
def handle_info(_event, redis) do
|
||||
{:noreply, [], redis}
|
||||
end
|
||||
end
|
|
@ -1,253 +0,0 @@
|
|||
defmodule Logger.Backends.FarmbotLogger do
|
||||
@moduledoc """
|
||||
Logger backend for logging to the frontend and dumping to the API.
|
||||
Takes messages that were logged useing Logger, if they can be
|
||||
jsonified, adds them too a buffer, when that buffer hits a certain
|
||||
size, it tries to dump the messages onto the API.
|
||||
"""
|
||||
alias Farmbot.{Context, HTTP}
|
||||
use GenEvent
|
||||
require Logger
|
||||
use Farmbot.DebugLog
|
||||
@save_path Application.get_env(:farmbot, :path) <> "/logs.txt"
|
||||
|
||||
# ten megs. i promise
|
||||
@max_file_size 1.0e+7
|
||||
@filtered "[FILTERED]"
|
||||
|
||||
@log_amnt 5
|
||||
|
||||
@typedoc """
|
||||
The state of the logger
|
||||
"""
|
||||
@type state :: %{logs: [log_message], posting: boolean, context: nil | Context.t}
|
||||
|
||||
@typedoc """
|
||||
Type of message for ticker
|
||||
"""
|
||||
@type rpc_log_type
|
||||
:: :success
|
||||
| :busy
|
||||
| :warn
|
||||
| :error
|
||||
| :info
|
||||
| :fun
|
||||
| :debug
|
||||
|
||||
@typedoc """
|
||||
Elixir Logger level type
|
||||
"""
|
||||
@type logger_level
|
||||
:: :info
|
||||
| :warn
|
||||
| :error
|
||||
| :debug
|
||||
|
||||
@typedoc """
|
||||
One day this will me more
|
||||
"""
|
||||
@type channel :: :toast | :email
|
||||
|
||||
@type log_message
|
||||
:: %{message: String.t,
|
||||
channels: [channel],
|
||||
created_at: integer,
|
||||
meta: %{type: rpc_log_type}}
|
||||
|
||||
def init(_) do
|
||||
# ctx = Farmbot.Context.new()
|
||||
debug_log "Starting Farmbot Logger"
|
||||
{:ok, %{logs: [], posting: false, context: nil}}
|
||||
end
|
||||
|
||||
# The example said ignore messages for other nodes, so im ignoring messages
|
||||
# for other nodes.
|
||||
def handle_event({_, gl, {Logger, _, _, _}}, state) when node(gl) != node() do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
# The logger event.
|
||||
def handle_event({_level, _, {Logger, _, _, _}}, %{context: nil} = state) do
|
||||
debug_log "Ignoreing message because no context"
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_event({level, _, {Logger, message, timestamp, metadata}}, state) do
|
||||
# if there is type key in the meta we need that to have priority
|
||||
type = Keyword.get(metadata, :type, level)
|
||||
channels = Keyword.get(metadata, :channels, [])
|
||||
created_at = parse_created_at(timestamp)
|
||||
san_m = sanitize(message, metadata, state)
|
||||
log = build_log(san_m, created_at, type, channels)
|
||||
:ok = GenServer.cast(state.context.transport, {:log, log})
|
||||
logs = [log | state.logs]
|
||||
if (!state.posting) and (Enum.count(logs) >= @log_amnt) do
|
||||
# If not already posting, and more than 50 messages
|
||||
spawn fn ->
|
||||
# debug_log "going to try to post"
|
||||
filterd_logs = Enum.filter(logs, fn(log) ->
|
||||
log.message != @filtered
|
||||
end)
|
||||
do_post(state.context, filterd_logs)
|
||||
end
|
||||
{:ok, %{state | logs: logs, posting: true}}
|
||||
else
|
||||
# debug_log "not posting and logs less than 50"
|
||||
{:ok, %{state | logs: logs}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:flush, state), do: {:ok, %{state | logs: [], posting: false}}
|
||||
|
||||
def handle_call(:post_success, state) do
|
||||
debug_log "Logs uploaded!"
|
||||
write_file(Enum.reverse(state.logs))
|
||||
{:ok, :ok, %{state | posting: false, logs: []}}
|
||||
end
|
||||
|
||||
def handle_call(:post_fail, state) do
|
||||
debug_log "Logs failed to upload!"
|
||||
{:ok, :ok, %{state | posting: false}}
|
||||
end
|
||||
|
||||
def handle_call(:messages, state) do
|
||||
{:ok, Enum.reverse(state.logs), state}
|
||||
end
|
||||
|
||||
def handle_call({:context, %Context{} = ctx}, state) do
|
||||
{:ok, :ok, %{state | context: ctx}}
|
||||
end
|
||||
|
||||
def handle_call(:get_state, state), do: {:ok, state, state}
|
||||
|
||||
@spec do_post(Context.t, [log_message]) :: no_return
|
||||
defp do_post(%Context{} = ctx, logs) do
|
||||
try do
|
||||
debug_log "doing post"
|
||||
{:ok, %{status_code: 200}} = HTTP.post(ctx, "/api/logs", Poison.encode!(logs))
|
||||
:ok = GenEvent.call(Elixir.Logger, __MODULE__, :post_success)
|
||||
rescue
|
||||
e ->
|
||||
debug_log "post failed: #{inspect e}"
|
||||
:ok = GenEvent.call(Elixir.Logger, __MODULE__, :post_fail)
|
||||
end
|
||||
end
|
||||
|
||||
@spec write_file([log_message]) :: no_return
|
||||
defp write_file(logs) do
|
||||
debug_log("Writing log file!")
|
||||
old = read_file()
|
||||
new_file = Enum.reduce(logs, old, fn(log, acc) ->
|
||||
if log.message != @filtered do
|
||||
acc <> log.message <> "\r\n"
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
bin = fifo(new_file)
|
||||
|
||||
Farmbot.System.FS.transaction fn ->
|
||||
File.write(@save_path, bin <> "\r\n")
|
||||
end
|
||||
end
|
||||
|
||||
# reads the current file.
|
||||
# if the file isnt found, returns an empty string
|
||||
@spec read_file :: binary
|
||||
defp read_file do
|
||||
case File.read(@save_path) do
|
||||
{:ok, bin} -> bin
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
# if the file is to big, fifo it
|
||||
# trim lines off the file until it is smaller than @max_file_size
|
||||
@spec fifo(binary) :: binary
|
||||
defp fifo(new_file) when byte_size(new_file) > @max_file_size do
|
||||
[_ | rest] = String.split(new_file, "\r\n")
|
||||
fifo(Enum.join(rest, "\r\n"))
|
||||
end
|
||||
|
||||
defp fifo(new_file), do: new_file
|
||||
|
||||
# if this backend crashes just pop it out of the logger backends.
|
||||
# if we don't do this it bacomes a huge mess because of Logger
|
||||
# trying to restart this module
|
||||
# then this module dying again
|
||||
# then printing a HUGE supervisor report
|
||||
# then Logger trying to add it again etc
|
||||
def terminate(_,_), do: spawn fn -> Logger.remove_backend(__MODULE__) end
|
||||
|
||||
@spec sanitize(binary, [any], state) :: String.t
|
||||
defp sanitize(message, meta, state) do
|
||||
module = Keyword.get(meta, :module)
|
||||
unless meta[:nopub] do
|
||||
message
|
||||
|> filter_module(module)
|
||||
|> filter_text(state)
|
||||
end
|
||||
end
|
||||
|
||||
@modules [
|
||||
:"Elixir.Nerves.InterimWiFi",
|
||||
:"Elixir.Nerves.NetworkInterface",
|
||||
:"Elixir.Nerves.InterimWiFi.WiFiManager.EventHandler",
|
||||
:"Elixir.Nerves.InterimWiFi.WiFiManager",
|
||||
:"Elixir.Nerves.InterimWiFi.DHCPManager",
|
||||
:"Elixir.Nerves.InterimWiFi.Udhcpc",
|
||||
:"Elixir.Nerves.NetworkInterface.Worker",
|
||||
:"Elixir.Nerves.InterimWiFi.DHCPManager.EventHandler",
|
||||
:"Elixir.Nerves.WpaSupplicant",
|
||||
]
|
||||
|
||||
for module <- @modules, do: defp filter_module(_, unquote(module)), do: @filtered
|
||||
defp filter_module(message, _module), do: message
|
||||
|
||||
defp filter_text(">>" <> m, state) do
|
||||
device = try do
|
||||
Farmbot.Database.Selectors.get_device(state.context)
|
||||
rescue
|
||||
_e in Farmbot.Database.Selectors.Error -> %{name: "Farmbot"}
|
||||
end
|
||||
|
||||
filter_text(device.name <> m, state)
|
||||
end
|
||||
|
||||
defp filter_text(m, _state) when is_binary(m) do
|
||||
try do
|
||||
Poison.encode!(m)
|
||||
m
|
||||
rescue
|
||||
_ -> @filtered
|
||||
end
|
||||
end
|
||||
defp filter_text(_message, _state), do: @filtered
|
||||
|
||||
# Couuld probably do this inline but wheres the fun in that. its a functional
|
||||
# language isn't it?
|
||||
# Takes Loggers time stamp and converts it into a unix timestamp.
|
||||
defp parse_created_at({{year, month, day}, {hour, minute, second, _mil}}) do
|
||||
%DateTime{year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {0,0},
|
||||
std_offset: 0,
|
||||
time_zone: "Etc/UTC",
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC"}
|
||||
|> DateTime.to_unix#(:milliseconds)
|
||||
end
|
||||
|
||||
@spec build_log(String.t, number, rpc_log_type, [channel]) :: log_message
|
||||
defp build_log(message, created_at, type, channels) do
|
||||
%{message: message,
|
||||
created_at: created_at,
|
||||
channels: channels,
|
||||
meta: %{type: type, x: 0, y: 0, z: 0}}
|
||||
end
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
defmodule Mix.Tasks.Farmbot.Discover do
|
||||
@moduledoc """
|
||||
Usage: mix farmbot.discover [OPTS]
|
||||
## OPTS
|
||||
* `--print` - print the output. (Optional)
|
||||
* `--format=FORMAT` - format to print. (Optional, ignored if not printing.)
|
||||
|
||||
## FORMAT
|
||||
* `json` - print as json.
|
||||
* `human` - print in human readable form. (default)
|
||||
|
||||
## Examples
|
||||
```
|
||||
$ mix farmbot.discover --print
|
||||
%{"cache-control": "max-age=1800", host: "192.168.29.103",
|
||||
location: "http://192.168.29.103/ssdp", node: "farmbot@nerves-7e4b",
|
||||
server: "Farmbot", service_name: "uuid:4d6ac078-0e4a-4a7a-b6cf-e951e5c959c5",
|
||||
st: "nerves:farmbot"}
|
||||
```
|
||||
|
||||
```
|
||||
$ mix farmbot.discover --print --format json
|
||||
[{"st":"nerves:farmbot","service_name":"uuid:4d6ac078-0e4a-4a7a-b6cf-e951e5c959c5","server":"Farmbot","node":"farmbot@nerves-7e4b","location":"http://192.168.29.103/ssdp","host":"192.168.29.103","cache-control":"max-age=1800"}]
|
||||
```
|
||||
"""
|
||||
use Mix.Task
|
||||
@shortdoc """
|
||||
Discovers farmbots on the network via ssdp.
|
||||
"""
|
||||
|
||||
def run(opts) do
|
||||
devices = Nerves.SSDPClient.discover()
|
||||
|> Enum.filter(fn({_, device}) -> device.st == "nerves:farmbot" end)
|
||||
|> Enum.map(fn({sn, device}) -> Map.put(device, :service_name, sn) end)
|
||||
|
||||
switches = [print: :boolean, format: :string]
|
||||
{kws, _, _} = OptionParser.parse(opts, switches: switches)
|
||||
should_print? = Keyword.get(kws, :print, false)
|
||||
if should_print? do
|
||||
format = Keyword.get(kws, :format, "human")
|
||||
do_print(devices, format)
|
||||
end
|
||||
|
||||
devices
|
||||
end
|
||||
|
||||
defp do_print(devices, "json") do
|
||||
IO.puts Poison.encode!(devices)
|
||||
end
|
||||
|
||||
defp do_print([device | rest], format) do
|
||||
print_device(device)
|
||||
do_print(rest, format)
|
||||
end
|
||||
|
||||
defp do_print([], _), do: :ok
|
||||
|
||||
defp print_device(device) do
|
||||
IO.inspect device
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
defmodule Mix.Tasks.Farmbot.Env do
|
||||
@moduledoc false
|
||||
use Mix.Task
|
||||
def run(_) do
|
||||
Mix.shell.info([:green, "Building initial environment for Farmbot OS"])
|
||||
Code.eval_file("scripts/generate_makefile.exs", File.cwd!)
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
defmodule Mix.Tasks.Farmbot.Sign do
|
||||
@moduledoc """
|
||||
usage: MIX_TARGET=rpi3 mix farmbot.sign ../fwup-key.priv
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
@shortdoc "Signs a fw image"
|
||||
|
||||
def run(args) do
|
||||
otp_app = Mix.Project.config[:app]
|
||||
target = Mix.Project.config[:target]
|
||||
fw_file = Path.join(["images", "#{Mix.env()}", "#{target}", "#{otp_app}.fw"])
|
||||
|
||||
case args do
|
||||
[priv_key_file, out_file] ->
|
||||
do_run(priv_key_file, fw_file, out_file)
|
||||
[priv_key_file] ->
|
||||
out_file = Path.join(["images", "#{Mix.env()}", "#{target}", "#{otp_app}-signed.fw"])
|
||||
do_run(priv_key_file, fw_file, out_file)
|
||||
_ -> Mix.raise("""
|
||||
Usage: mix farmbot.sign /tmp/fwup-key.priv /tmp/outputfile-signed.fw
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defp do_run(priv_key_file, unsigned_file, out_file) do
|
||||
Mix.shell.info [:green, "Signing: #{unsigned_file} with: #{priv_key_file} to: #{out_file}"]
|
||||
unless File.exists?(unsigned_file) do
|
||||
Mix.raise "Could not find Firmware!"
|
||||
end
|
||||
System.cmd("fwup", ["-S", "-s", priv_key_file, "-i", unsigned_file, "-o", out_file])
|
||||
end
|
||||
end
|
|
@ -1,83 +0,0 @@
|
|||
defmodule Mix.Tasks.Farmbot.Slack do
|
||||
@moduledoc "Upload a file to slack. Requires a slack key env var."
|
||||
use Mix.Task
|
||||
alias Farmbot.{Context, HTTP}
|
||||
@shortdoc "Upload an image to farmbot slack."
|
||||
|
||||
def run(opts) do
|
||||
Registry.start_link(:duplicate, Farmbot.Registry)
|
||||
Application.ensure_all_started(:httpoison)
|
||||
|
||||
{keywords, uhh, _} = opts |> OptionParser.parse(switches: [signed: :boolean])
|
||||
signed? = Keyword.get(keywords, :signed, false)
|
||||
{:ok, _} = Farmbot.DebugLog.start_link()
|
||||
ctx = Context.new()
|
||||
{:ok, http} = HTTP.start_link(ctx, [])
|
||||
ctx = %{ctx | http: http}
|
||||
otp_app = Mix.Project.config[:app]
|
||||
target = Mix.Project.config[:target]
|
||||
commit = Mix.Project.config[:commit]
|
||||
fw_file = Path.join(["images", "#{Mix.env()}", "#{target}",
|
||||
(if signed?, do: "#{otp_app}-signed.fw", else: "#{otp_app}.fw")])
|
||||
|
||||
unless File.exists?(fw_file) do
|
||||
Mix.raise "Could not find firmware: #{fw_file}"
|
||||
end
|
||||
|
||||
token = System.get_env("SLACK_TOKEN") || Mix.raise "Could not find SLACK_TOKEN"
|
||||
time = format_date_time(File.stat!(fw_file))
|
||||
filename = "#{otp_app}-#{Mix.env()}-#{commit}#{if signed?, do: "-signed-", else: "-"}#{time}.fw"
|
||||
comment = Enum.join(uhh, " ")
|
||||
|
||||
Mix.shell.info [:green, "Uploading: #{filename} (#{fw_file})"]
|
||||
|
||||
url = "https://slack.com/api/files.upload"
|
||||
form_data = %{
|
||||
:file => fw_file,
|
||||
"token" => token,
|
||||
"channels" => "embedded-systems",
|
||||
"filename" => filename,
|
||||
"title" => filename,
|
||||
"initial_comment" => """
|
||||
*New Farmbot Firmware!*
|
||||
> *_Env_*: `#{Mix.env()}`
|
||||
> *_Target_*: `#{target}`
|
||||
> *_Commit_*: `#{commit}`
|
||||
> *_Time_*: `#{time}`
|
||||
#{String.trim(comment)}
|
||||
"""
|
||||
}
|
||||
payload = Enum.map(form_data, fn({key, val}) -> {key, val} end)
|
||||
real_payload = {:multipart, payload}
|
||||
headers = [
|
||||
{'User-Agent', 'Farmbot HTTP Adapter'}
|
||||
]
|
||||
%{body: body} = HTTP.post!(ctx, url, real_payload, headers)
|
||||
ok? = body |> Poison.decode!() |> Map.get("ok", false)
|
||||
notify_opts = ~w"""
|
||||
-u normal -i #{:code.priv_dir(otp_app)}/static/farmbot_logo.png -a farmbot_build
|
||||
"""
|
||||
message = if ok?, do: "Upload completed", else: "Upload failed!"
|
||||
System.cmd("notify-send", [notify_opts | ["Farmbot Uploader", message]] |> List.flatten())
|
||||
end
|
||||
|
||||
defp format_date_time(%{mtime: {{yr,m,day}, {hr, min, sec}}}) do
|
||||
dt = %DateTime{
|
||||
hour: hr,
|
||||
year: yr,
|
||||
month: m,
|
||||
day: day,
|
||||
minute: min,
|
||||
second: sec,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
std_offset: 0,
|
||||
utc_offset: 0
|
||||
} |> Timex.local()
|
||||
"#{dt.year}-#{pad(dt.month)}-#{pad(dt.day)}_#{pad(dt.hour)}#{pad(dt.minute)}"
|
||||
end
|
||||
|
||||
defp pad(int) do
|
||||
if int < 10, do: "0#{int}", else: "#{int}"
|
||||
end
|
||||
end
|
|
@ -1,71 +0,0 @@
|
|||
defmodule Mix.Tasks.Farmbot.Upload do
|
||||
@moduledoc false
|
||||
use Mix.Task
|
||||
@shortdoc "Uploads a file to a url"
|
||||
|
||||
def run(opts) do
|
||||
otp_app = Mix.Project.config[:app]
|
||||
target = Mix.Project.config[:target]
|
||||
|
||||
{keywords, ip_address} =
|
||||
case opts |> OptionParser.parse(switches: [signed: :boolean]) do
|
||||
{keywords, [ip_address], _} -> {keywords, ip_address}
|
||||
{keywords, [], _} -> {keywords, try_discover()}
|
||||
end
|
||||
|
||||
signed_bool = Keyword.get(keywords, :signed, false)
|
||||
file_name = Path.join(["images", "#{Mix.env()}", "#{target}", find_file_name(otp_app, signed_bool)])
|
||||
|
||||
unless File.exists?(file_name) do
|
||||
Mix.raise("Could not find firmware: #{file_name}")
|
||||
end
|
||||
|
||||
Mix.shell.info "Trying to uploading firmware: #{file_name}"
|
||||
|
||||
Application.ensure_all_started(:httpoison)
|
||||
|
||||
context = Farmbot.Context.new()
|
||||
Registry.start_link(:duplicate, Farmbot.Registry)
|
||||
{:ok, _} = Farmbot.DebugLog.start_link()
|
||||
{:ok, http} = Farmbot.HTTP.start_link(context, [])
|
||||
context = %{context | http: http}
|
||||
url = "http://#{ip_address}"
|
||||
ping_url = "#{url}/api/ping"
|
||||
upload_url = "#{url}/api/upload_firmware"
|
||||
Farmbot.HTTP.get! context, ping_url
|
||||
Mix.shell.info [:green, "Connected to bot."]
|
||||
|
||||
payload = [
|
||||
{:file, file_name}
|
||||
]
|
||||
# payload = [{:file, file_name}]
|
||||
Mix.shell.info "Uploading..."
|
||||
# headers = ["content-type", "http/form_data"]
|
||||
r = Farmbot.HTTP.post! context, upload_url, {:multipart, payload}, [], timeout: :infinity
|
||||
|
||||
unless match?(%{status_code: 200}, r) do
|
||||
Mix.raise "Failed to upload firmware: #{format r}"
|
||||
end
|
||||
|
||||
Mix.shell.info [:green, "Finished!"]
|
||||
end
|
||||
|
||||
defp format(%{body: body}), do: body
|
||||
defp format(other), do: inspect other
|
||||
|
||||
defp find_file_name(otp_app, true), do: "#{otp_app}-signed.fw"
|
||||
defp find_file_name(otp_app, false), do: "#{otp_app}.fw"
|
||||
|
||||
defp try_discover do
|
||||
devices = Mix.Tasks.Farmbot.Discover.run([])
|
||||
case devices do
|
||||
[device] -> device.host
|
||||
[_device | _more] -> do_raise("detected more than one farmbot.")
|
||||
[] -> do_raise("could not detect farmbot.")
|
||||
end
|
||||
end
|
||||
|
||||
defp do_raise(msg) do
|
||||
Mix.raise "#{msg} Please supply the ip address of your bot."
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
defmodule Mix.Tasks.Farmbot.Warning do
|
||||
@moduledoc false
|
||||
use Mix.Task
|
||||
def run(_), do: Mix.raise("Please export a MIX_TARGET")
|
||||
end
|
9
mix.exs
9
mix.exs
|
@ -30,6 +30,7 @@ defmodule Farmbot.Mixfile do
|
|||
images_path: "images/#{Mix.env()}/#{@target}",
|
||||
config_path: "config/config.exs",
|
||||
lockfile: "mix.lock",
|
||||
elixirc_paths: elixirc_paths(Mix.env, @target),
|
||||
compilers: Mix.compilers ++ maybe_use_webpack(),
|
||||
aliases: aliases(@target),
|
||||
deps: deps() ++ system(@target),
|
||||
|
@ -190,7 +191,6 @@ defmodule Farmbot.Mixfile do
|
|||
]
|
||||
end
|
||||
|
||||
|
||||
# TODO(connor): Build this into `:ex_webpack`
|
||||
defp maybe_use_webpack() do
|
||||
case System.get_env("NO_WEBPACK") do
|
||||
|
@ -199,6 +199,13 @@ defmodule Farmbot.Mixfile do
|
|||
end
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test, "host") do
|
||||
["./lib", "./nerves/host", "./test/support"]
|
||||
end
|
||||
|
||||
defp elixirc_paths(_env, target) do
|
||||
["./lib", "./nerves/#{target}"]
|
||||
end
|
||||
|
||||
# this is for cross compilation to work
|
||||
# New version of nerves might not need this?
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
defmodule Farmbot.AuthTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.{Context, Auth}
|
||||
|
||||
setup_all do
|
||||
context = Context.new()
|
||||
new_context = Farmbot.Test.Helpers.Context.replace_http(context)
|
||||
{:ok, auth_pid} = Auth.start_link(new_context, [])
|
||||
[cs_context: %{new_context | auth: auth_pid}]
|
||||
end
|
||||
|
||||
setup %{cs_context: context} do
|
||||
:ok = Auth.purge_token(context.auth)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "returns nil if no token", %{cs_context: context} do
|
||||
thing = Auth.get_token(context.auth)
|
||||
assert thing == nil
|
||||
end
|
||||
|
||||
test "logs in", %{cs_context: context} do
|
||||
{:ok, login_token} = Farmbot.Auth.try_log_in(context.auth)
|
||||
assert match?(%Farmbot.Token{}, login_token)
|
||||
end
|
||||
|
||||
test "gets the current token", %{cs_context: context} do
|
||||
good_interim(context.auth)
|
||||
{:ok, login_token} = Farmbot.Auth.try_log_in(context.auth)
|
||||
{:ok, token} = Auth.get_token(context.auth)
|
||||
assert login_token == token
|
||||
end
|
||||
|
||||
test "gets the current server", %{cs_context: context} do
|
||||
good_interim(context.auth)
|
||||
{:ok, server} = Auth.get_server(context.auth)
|
||||
assert server == "http://localhost:3000"
|
||||
end
|
||||
|
||||
# test "doesnt get a token on bad creds", %{cs_context: context} do
|
||||
# bad_interim(context.auth)
|
||||
# {:error, reason} = Auth.try_log_in(context.auth)
|
||||
# assert reason == :bad_password
|
||||
# end
|
||||
|
||||
test "logs in aggressivly", %{cs_context: context} do
|
||||
good_interim(context.auth)
|
||||
{:ok, login_token} = Farmbot.Auth.try_log_in!(context.auth)
|
||||
assert match?(%Farmbot.Token{}, login_token)
|
||||
end
|
||||
|
||||
test "logs in with creds, then with a secret", %{cs_context: context} do
|
||||
good_interim(context.auth)
|
||||
{:ok, login_token} = Farmbot.Auth.try_log_in(context.auth)
|
||||
assert match?(%Farmbot.Token{}, login_token)
|
||||
|
||||
Process.sleep(100)
|
||||
|
||||
{:ok, new_token} = Farmbot.Auth.try_log_in(context.auth)
|
||||
assert match?(%Farmbot.Token{}, new_token)
|
||||
end
|
||||
|
||||
# test "factory resets the bot on bad log in", %{cs_context: context} do
|
||||
# use_cassette "bad_login" do
|
||||
# bad_interim(context.auth)
|
||||
# with_mock Farmbot.System, [factory_reset: fn(_reason) -> :ok end] do
|
||||
# Auth.try_log_in!(context.auth)
|
||||
# assert called Farmbot.System.factory_reset(:_)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
# test "keeps a backup in case of catastrophic issues secret", %{cs_context: context}do
|
||||
# Auth.purge_token(context.auth)
|
||||
# use_cassette "good_login" do
|
||||
# good_interim(context.auth)
|
||||
# {:ok, login_token} = Farmbot.Auth.try_log_in!(context.auth)
|
||||
# assert match?(%Farmbot.Token{}, login_token)
|
||||
#
|
||||
# assert_raise RuntimeError, fn() ->
|
||||
# GenServer.stop(context.auth, :uhhhh?)
|
||||
# end
|
||||
# File.rm("/tmp/secret") # this is the good file
|
||||
# Process.sleep(500)
|
||||
#
|
||||
# {:ok, token} = Farmbot.Auth.try_log_in!(context.auth)
|
||||
# assert match?(%Farmbot.Token{}, token)
|
||||
# end
|
||||
# end
|
||||
|
||||
# test "forces a new token" do
|
||||
# Auth.purge_token
|
||||
# use_cassette "good_login" do
|
||||
# good_interim(context.auth)
|
||||
# {:ok, login_token} = Farmbot.Auth.try_log_in!
|
||||
# assert match?(%Farmbot.Token{}, login_token)
|
||||
# end
|
||||
#
|
||||
# use_cassette "good_login_2" do
|
||||
# send Farmbot.Auth, :new_token
|
||||
# {:ok, token} = Farmbot.Auth.get_token
|
||||
# assert match?(%Farmbot.Token{}, token)
|
||||
# end
|
||||
# end
|
||||
|
||||
defp good_interim(auth) do
|
||||
:ok = Auth.interim(auth, "admin@admin.com", "password123", "http://localhost:3000")
|
||||
end
|
||||
|
||||
defp bad_interim(auth) do
|
||||
:ok = Auth.interim(auth, "fail@fail.org", "password", "http://localhost:3000")
|
||||
end
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
defmodule Farmbot.BotState.ConfigurationTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
cs_context = Context.new()
|
||||
[cs_context: cs_context]
|
||||
end
|
||||
|
||||
test "makes sure we dont mess state up with bad calls or casts", context do
|
||||
before_call = get_state(context.cs_context)
|
||||
resp = GenServer.call(context.cs_context.configuration, :do_a_barrel_roll)
|
||||
after_call = get_state(context.cs_context)
|
||||
assert(resp == :unhandled)
|
||||
assert(after_call == before_call)
|
||||
|
||||
GenServer.cast(context.cs_context.configuration, :bot_net_start)
|
||||
after_cast = get_state(context.cs_context)
|
||||
assert(before_call == after_cast)
|
||||
end
|
||||
|
||||
test "updates a setting inside informational settings", context do
|
||||
conf = context.cs_context.configuration
|
||||
old = get_state(context.cs_context)
|
||||
GenServer.cast(conf, {:update_info, :i_know_this, :its_unix})
|
||||
# maybe bug? change this cast to a call?
|
||||
new = get_state(context.cs_context)
|
||||
assert(old != new)
|
||||
end
|
||||
|
||||
defp get_state(ctx) do
|
||||
Process.sleep(10)
|
||||
:sys.get_state(ctx.monitor).state |> Map.fetch!(:configuration)
|
||||
end
|
||||
end
|
|
@ -1,146 +0,0 @@
|
|||
defmodule Farmbot.BotStateTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
context = Context.new()
|
||||
[cs_context: context]
|
||||
end
|
||||
|
||||
test "Gets the current bot position", %{cs_context: context} do
|
||||
[x,y,z] = Farmbot.BotState.get_current_pos(context)
|
||||
assert(is_integer(x) and is_integer(y) and is_integer(z))
|
||||
end
|
||||
|
||||
test "Sets a new position", %{cs_context: context} do
|
||||
Farmbot.BotState.set_pos(context, 45, 123, -666)
|
||||
[x,y,z] = Farmbot.BotState.get_current_pos(context)
|
||||
assert(x == 45)
|
||||
assert(y == 123)
|
||||
assert(z == -666)
|
||||
end
|
||||
|
||||
test "sets a pin mode", %{cs_context: context} do
|
||||
Farmbot.BotState.set_pin_mode(context, 123, 0)
|
||||
%{mode: mode, value: _} = Farmbot.BotState.get_pin(context, 123)
|
||||
assert(mode == 0)
|
||||
end
|
||||
|
||||
test "sets a pin value", %{cs_context: context} do
|
||||
Farmbot.BotState.set_pin_value(context, 123, 55)
|
||||
%{mode: _, value: value} = Farmbot.BotState.get_pin(context, 123)
|
||||
assert(value == 55)
|
||||
end
|
||||
|
||||
test "updates tons of configs", %{cs_context: context} do
|
||||
true = Farmbot.BotState.update_config(context, "os_auto_update", false)
|
||||
os_auto_update = Farmbot.BotState.get_config(context, :os_auto_update)
|
||||
assert(os_auto_update == false)
|
||||
|
||||
true = Farmbot.BotState.update_config(context, "steps_per_mm_x", 123)
|
||||
true = Farmbot.BotState.update_config(context, "steps_per_mm_y", 456)
|
||||
true = Farmbot.BotState.update_config(context, "steps_per_mm_z", 789)
|
||||
|
||||
x = Farmbot.BotState.get_config(context, :steps_per_mm_x)
|
||||
y = Farmbot.BotState.get_config(context, :steps_per_mm_y)
|
||||
z = Farmbot.BotState.get_config(context, :steps_per_mm_z)
|
||||
assert([x,y,z] == [123,456,789])
|
||||
|
||||
fail = Farmbot.BotState.update_config(context, "self_destruct_count_down", 10_000)
|
||||
assert(fail == false)
|
||||
end
|
||||
|
||||
test "gets the current os version", %{cs_context: context} do
|
||||
# i just want the coverage report ok
|
||||
os = Farmbot.BotState.get_os_version(context)
|
||||
assert(is_bitstring(os))
|
||||
end
|
||||
|
||||
test "sets end stops", %{cs_context: context} do
|
||||
es = {1,1,1,1,1,1}
|
||||
Farmbot.BotState.set_end_stops(context, es)
|
||||
assert(get_hardware_part(:end_stops, context) == es)
|
||||
end
|
||||
|
||||
test "gets all the mcu params", %{cs_context: context} do
|
||||
assert get_hardware_part(:mcu_params, context) == Farmbot.BotState.get_all_mcu_params(context)
|
||||
end
|
||||
|
||||
test "sets firmware version", %{cs_context: context} do
|
||||
Farmbot.BotState.set_fw_version(context, "uhhhhh")
|
||||
Process.sleep(100)
|
||||
assert Farmbot.BotState.get_fw_version(context) == "uhhhhh"
|
||||
end
|
||||
|
||||
test "sets some farmware env vars", %{cs_context: context} do
|
||||
r = Farmbot.BotState.set_user_env(context, %{"SUPER_COOL_VAR" => 123})
|
||||
assert(r == true)
|
||||
abc = Farmbot.BotState.set_user_env(context, "DIFFERENT_COOL_VAR", "String value")
|
||||
assert(abc == true)
|
||||
end
|
||||
|
||||
test "locks the bot then unlocks it", %{cs_context: context} do
|
||||
:ok = Farmbot.BotState.lock_bot(context)
|
||||
stuffA = get_config_part(:informational_settings, context)[:locked]
|
||||
lockedA? = Farmbot.BotState.locked?(context)
|
||||
assert stuffA == true
|
||||
assert stuffA == lockedA?
|
||||
|
||||
:ok = Farmbot.BotState.unlock_bot(context)
|
||||
stuffB = get_config_part(:informational_settings, context)[:locked]
|
||||
lockedB? = Farmbot.BotState.locked?(context)
|
||||
assert stuffB == false
|
||||
assert stuffB == lockedB?
|
||||
end
|
||||
|
||||
test "sets sync msg to :synced", %{cs_context: context} do
|
||||
context = %{context | serial: nil}
|
||||
:ok = Farmbot.BotState.set_sync_msg(context, :synced)
|
||||
thing = get_config_part(:informational_settings, context)[:sync_status]
|
||||
assert thing == :synced
|
||||
end
|
||||
|
||||
test "sets sync msg to :sync_now", %{cs_context: context} do
|
||||
:ok = Farmbot.BotState.set_sync_msg(context, :sync_now)
|
||||
thing = get_config_part(:informational_settings, context)[:sync_status]
|
||||
assert thing == :sync_now
|
||||
end
|
||||
|
||||
test "sets sync msg to :syncing", %{cs_context: context} do
|
||||
:ok = Farmbot.BotState.set_sync_msg(context, :syncing)
|
||||
thing = get_config_part(:informational_settings, context)[:sync_status]
|
||||
assert thing == :syncing
|
||||
end
|
||||
|
||||
test "sets sync msg to :sync_error", %{cs_context: context} do
|
||||
:ok = Farmbot.BotState.set_sync_msg(context, :sync_error)
|
||||
thing = get_config_part(:informational_settings, context)[:sync_status]
|
||||
assert thing == :sync_error
|
||||
end
|
||||
|
||||
test "sets sync msg to :unknown", %{cs_context: context} do
|
||||
:ok = Farmbot.BotState.set_sync_msg(context, :unknown)
|
||||
thing = get_config_part(:informational_settings, context)[:sync_status]
|
||||
assert thing == :unknown
|
||||
end
|
||||
|
||||
test "raises an error if wrong sync message", %{cs_context: context} do
|
||||
assert_raise(FunctionClauseError, fn() ->
|
||||
Farmbot.BotState.set_sync_msg(context, "some str?")
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_hardware_part(part, context) do
|
||||
Process.sleep(10)
|
||||
:sys.get_state(context.monitor).state
|
||||
|> Map.get(:hardware)
|
||||
|> Map.get(part)
|
||||
end
|
||||
|
||||
defp get_config_part(part, context) do
|
||||
Process.sleep(10)
|
||||
:sys.get_state(context.monitor).state
|
||||
|> Map.get(:configuration)
|
||||
|> Map.get(part)
|
||||
end
|
||||
end
|
|
@ -1,98 +0,0 @@
|
|||
defmodule AstTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.CeleryScript.Ast, as: Ast
|
||||
|
||||
test "parses an ast from a stringed map" do
|
||||
test_ast =
|
||||
%{"args" => %{"message" => "hello world"},
|
||||
"body" =>[],
|
||||
"kind" => "send_message"}
|
||||
ast = Ast.parse(test_ast)
|
||||
assert(ast.args.message == "hello world")
|
||||
assert(ast.body == [])
|
||||
assert(ast.kind == "send_message")
|
||||
end
|
||||
|
||||
test "parses an ast from a stringed map with no body" do
|
||||
test_ast =
|
||||
%{"args" => %{"coords" => [1,2,3]},
|
||||
"kind" => "pickup_truck"}
|
||||
ast = Ast.parse(test_ast)
|
||||
assert(ast.args.coords == [1,2,3])
|
||||
assert(ast.body == [])
|
||||
assert(ast.kind == "pickup_truck")
|
||||
end
|
||||
|
||||
test "parses an ast from a keyed map" do
|
||||
test_ast_a =
|
||||
%{args: %{},
|
||||
kind: "play_kick_ball"}
|
||||
ast_a = Ast.parse(test_ast_a)
|
||||
assert(ast_a.args == %{})
|
||||
assert(ast_a.body == [])
|
||||
assert(ast_a.kind == "play_kick_ball")
|
||||
|
||||
test_ast_b =
|
||||
%{args: %{},
|
||||
body: [],
|
||||
kind: "play_kick_ball"}
|
||||
ast_b = Ast.parse(test_ast_b)
|
||||
assert(ast_b.args == %{})
|
||||
assert(ast_b.body == [])
|
||||
assert(ast_b.kind == "play_kick_ball")
|
||||
end
|
||||
|
||||
test "creats more ast nodes from the body" do
|
||||
test_ast_body_inner = %{
|
||||
args: %{},
|
||||
body: [],
|
||||
kind: "inner_body_node"
|
||||
}
|
||||
|
||||
test_ast_body_main = %{
|
||||
args: %{},
|
||||
body: [test_ast_body_inner, test_ast_body_inner],
|
||||
kind: "body_node"
|
||||
}
|
||||
|
||||
test_ast_main =
|
||||
%{args: %{},
|
||||
body: [test_ast_body_main, test_ast_body_inner, test_ast_body_main],
|
||||
kind: "play_kick_ball"}
|
||||
main_ast = Ast.parse(test_ast_main)
|
||||
assert(main_ast.args == %{})
|
||||
assert(main_ast.kind == "play_kick_ball")
|
||||
body = main_ast.body
|
||||
assert(is_list(body))
|
||||
assert(Enum.count(body) == 3)
|
||||
|
||||
# there may need to be a protection for a stack overflow here lol
|
||||
# if you could somehow make a node that references itself or someting
|
||||
# similar to that it would recurse forever
|
||||
random_ast_node = Enum.random(body)
|
||||
assert(random_ast_node.kind |> is_bitstring)
|
||||
end
|
||||
|
||||
test "manually creates an ast" do
|
||||
Ast.create("speak_spanish", %{words: "hello"}, body: [])
|
||||
end
|
||||
|
||||
test "raises an error on bad data" do
|
||||
maybe_ast = "some arbitrary data"
|
||||
assert_raise Farmbot.CeleryScript.Error, fn() ->
|
||||
Ast.parse(maybe_ast)
|
||||
end
|
||||
end
|
||||
|
||||
test "does recursion" do
|
||||
map = %{"kind" => "thing",
|
||||
"args" => %{"arg_a" => "string",
|
||||
"arg_b" => %{"kind" => "thingb",
|
||||
"args" => %{}, "body" => []}},
|
||||
"body" => []}
|
||||
ast = Ast.parse(map)
|
||||
assert map["kind"] == ast.kind
|
||||
assert map["args"]["arg_b"]["kind"] == ast.args.arg_b.kind
|
||||
end
|
||||
|
||||
end
|
|
@ -1,126 +0,0 @@
|
|||
defmodule CommandTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.CeleryScript.{Command, Ast, Error}
|
||||
alias Farmbot.Context
|
||||
alias Farmbot.Database
|
||||
alias Farmbot.Database.Selectors.Error, as: SelectorError
|
||||
alias Database.Syncable.Point
|
||||
import Mock
|
||||
|
||||
setup_all do
|
||||
[cs_context: Farmbot.Context.new()]
|
||||
end
|
||||
|
||||
test "doesnt freak out on no instruction", %{cs_context: context} do
|
||||
json = ~s"""
|
||||
{
|
||||
"kind": "bring_down_network",
|
||||
"args": {"time": "now"}
|
||||
}
|
||||
"""
|
||||
cs = Poison.decode!(json) |> Ast.parse()
|
||||
assert_raise Error, fn() ->
|
||||
Command.do_command(cs, context)
|
||||
end
|
||||
end
|
||||
|
||||
test "rescues from bad things", %{cs_context: context} do
|
||||
with_mock Farmbot.CeleryScript.Command.Nothing, [run: fn(_,_,_) ->
|
||||
raise Error, message: "Mucking About with Mocks"
|
||||
end] do
|
||||
assert_raise Error, fn() ->
|
||||
nothing_ast = %Ast{kind: "nothing", args: %{}, body: [], comment: "hey!"}
|
||||
Command.do_command(nothing_ast, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "cant execute random data that is not a cs node", %{cs_context: context} do
|
||||
assert_raise Error, fn() ->
|
||||
Command.do_command("shut the door its cold in here!", context)
|
||||
end
|
||||
end
|
||||
|
||||
test "converts a coordinate to coordinates", %{cs_context: context} do
|
||||
# a coordinate is already a coordinate
|
||||
ast_a = %Ast{kind: "coordinate", args: %{x: 1, y: 1, z: 1}, body: []}
|
||||
context = Command.ast_to_coord(context, ast_a)
|
||||
{coord_a, next_context} = Farmbot.Context.pop_data(context)
|
||||
|
||||
assert is_map(coord_a)
|
||||
|
||||
assert is_map(next_context)
|
||||
|
||||
assert ast_a.kind == coord_a.kind
|
||||
end
|
||||
|
||||
test "converts a tool to a coordinate" do
|
||||
ctx = Context.new()
|
||||
{:ok, database} = Database.start_link(ctx, [])
|
||||
ctx = %{ctx | database: database}
|
||||
|
||||
tool_id = 66
|
||||
|
||||
point = %Point{id: 6, x: 1, y: 2, z: 3, pointer_type: "tool_slot", tool_id: tool_id}
|
||||
|
||||
Database.commit_records point, ctx, Point
|
||||
|
||||
tool_ast = %Ast{kind: "tool", args: %{tool_id: tool_id}, body: []}
|
||||
new_context = Command.ast_to_coord(ctx, tool_ast)
|
||||
{coord, _new_context2} = Context.pop_data(new_context)
|
||||
assert coord.args.x == point.x
|
||||
assert coord.args.y == point.y
|
||||
assert coord.args.z == point.z
|
||||
end
|
||||
|
||||
test "raises if there is no tool_slot or something" do
|
||||
ctx = Context.new()
|
||||
{:ok, pid} = Database.start_link(ctx, [])
|
||||
|
||||
ctx = %{ctx | database: pid}
|
||||
|
||||
tool_ast = %Ast{kind: "tool", args: %{tool_id: 905}, body: []}
|
||||
assert_raise SelectorError, "Could not find tool_slot with tool_id: 905", fn() ->
|
||||
Command.ast_to_coord(ctx, tool_ast)
|
||||
end
|
||||
end
|
||||
|
||||
test "gives the origin on nothing", %{cs_context: context} do
|
||||
nothing = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
{coord, context} = Command.ast_to_coord(context, nothing) |> Farmbot.Context.pop_data
|
||||
assert is_map(coord)
|
||||
assert is_map(context)
|
||||
assert coord.args.x == 0
|
||||
assert coord.args.y == 0
|
||||
assert coord.args.z == 0
|
||||
end
|
||||
|
||||
test "gives an error on unknown asts", %{cs_context: context} do
|
||||
blerp = %Ast{kind: "blerp", args: %{}, body: []}
|
||||
assert_raise Error, fn ->
|
||||
Command.ast_to_coord(context, blerp)
|
||||
end
|
||||
end
|
||||
|
||||
# test "doesnt implode if a sequence relies on itself" do
|
||||
# use Amnesia
|
||||
# use Sequence
|
||||
#
|
||||
# real_sequence = %Sequence{args: %{"is_outdated" => false,
|
||||
# "version" => 4},
|
||||
# body: [%{"args" => %{"_else" => %{"args" => %{}, "kind" => "nothing"},
|
||||
# "_then" => %{"args" => %{"sequence_id" => 40000}, "kind" => "execute"},
|
||||
# "lhs" => "x", "op" => "not", "rhs" => 10000}, "kind" => "_if"}],
|
||||
# color: "gray", id: 40000, kind: "sequence", name: "seq_a"}
|
||||
#
|
||||
# Amnesia.transaction do
|
||||
# real_sequence |> Sequence.write
|
||||
# end
|
||||
#
|
||||
# sequence = %Ast{kind: "execute", args: %{sequence_id: 40000}, body: []}
|
||||
#
|
||||
# assert_raise(Error, "TO MUCH RECURSION", fn() ->
|
||||
# Command.do_command(sequence)
|
||||
# end)
|
||||
# end
|
||||
end
|
|
@ -1,112 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.IfTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.CeleryScript.{Ast, Command, Error}
|
||||
alias Command.If
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
[cs_context: ctx]
|
||||
end
|
||||
|
||||
test "raises when given a bad left hand side", %{cs_context: context} do
|
||||
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
lhs = "some arbitrary property"
|
||||
rhs = 1234
|
||||
op = ">"
|
||||
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
|
||||
ast = %Ast{kind: "_if", args: args, body: []}
|
||||
|
||||
assert_raise Error, "Got unexpected left hand side of IF: some arbitrary property", fn() ->
|
||||
If.run(ast.args, ast.body, context)
|
||||
end
|
||||
end
|
||||
|
||||
test "raises if bad operator", %{cs_context: context} do
|
||||
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
lhs = "x"
|
||||
rhs = 1234
|
||||
op = "almost greater than but not really"
|
||||
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
|
||||
ast = %Ast{kind: "_if", args: args, body: []}
|
||||
|
||||
assert_raise Error, "Bad operator in if #{inspect op}", fn() ->
|
||||
If.run(ast.args, ast.body, context)
|
||||
end
|
||||
end
|
||||
|
||||
test "does if with an axis", %{cs_context: context} do
|
||||
[100 ,2,3] = Farmbot.BotState.set_pos(context, 100, 2, 3)
|
||||
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
else_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
|
||||
lhs = "z"
|
||||
rhs = 0
|
||||
op = ">"
|
||||
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
|
||||
ast = %Ast{kind: "_if", args: args, body: []}
|
||||
|
||||
new_context = If.run(ast.args, ast.body, context)
|
||||
{results, _new_context2} = Context.pop_data(new_context)
|
||||
|
||||
assert results == then_ast
|
||||
refute results == else_ast
|
||||
|
||||
end
|
||||
|
||||
test "does if with a pin", %{cs_context: context} do
|
||||
pin_num = 12
|
||||
Farmbot.BotState.set_pin_mode(context, pin_num, 1)
|
||||
Farmbot.BotState.set_pin_value(context, pin_num, 123)
|
||||
|
||||
then_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
else_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
|
||||
lhs = "pin#{pin_num}"
|
||||
op = "<"
|
||||
rhs = 9000
|
||||
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
|
||||
ast = %Ast{kind: "_if", args: args, body: []}
|
||||
|
||||
new_context = If.run(ast.args, ast.body, context)
|
||||
{results, _new_context2} = Context.pop_data(new_context)
|
||||
|
||||
assert results == then_ast
|
||||
refute results == else_ast
|
||||
end
|
||||
|
||||
test "does else if", %{cs_context: context} do
|
||||
[100 ,2,3] = Farmbot.BotState.set_pos(context, 100, 2, 3)
|
||||
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
then_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
|
||||
lhs = "y"
|
||||
rhs = 0
|
||||
op = "is"
|
||||
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
|
||||
ast = %Ast{kind: "_if", args: args, body: []}
|
||||
|
||||
new_context = If.run(ast.args, ast.body, context)
|
||||
{results, _new_context2} = Context.pop_data(new_context)
|
||||
|
||||
assert results == else_ast
|
||||
refute results == then_ast
|
||||
end
|
||||
|
||||
test "does else if with not", %{cs_context: context} do
|
||||
[100 ,2,3] = Farmbot.BotState.set_pos(context, 100, 2, 3)
|
||||
else_ast = %Ast{kind: "nothing", args: %{}, body: []}
|
||||
then_ast = %Ast{kind: "this is fake and unsafe", args: %{}, body: []}
|
||||
lhs = "y"
|
||||
rhs = 2
|
||||
op = "not"
|
||||
args = %{_else: else_ast, _then: then_ast, lhs: lhs, rhs: rhs, op: op}
|
||||
ast = %Ast{kind: "_if", args: args, body: []}
|
||||
|
||||
new_context = If.run(ast.args, ast.body, context)
|
||||
{results, _new_context2} = Context.pop_data(new_context)
|
||||
|
||||
assert results == else_ast
|
||||
refute results == then_ast
|
||||
end
|
||||
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.CheckUpdatesTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.CeleryScript.{Ast, Command, Error}
|
||||
import Mock
|
||||
|
||||
test "doesnt check for arduino updates anymore" do
|
||||
ctx = Farmbot.Context.new()
|
||||
ast = %Ast{kind: "check_updates", args: %{package: "arduino_firmware"}, body: []}
|
||||
assert_raise Error, "arduino firmware is now bundled into the OS.", fn() ->
|
||||
Command.do_command(ast, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
test "doesnt know what to do with other strings" do
|
||||
ctx = Farmbot.Context.new()
|
||||
ast = %Ast{kind: "check_updates", args: %{package: "explorer.exe"}, body: []}
|
||||
assert_raise Error, "unknown package: #{ast.args.package}", fn() ->
|
||||
Command.do_command(ast, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
test "does update check for fbos" do
|
||||
# the real update thing will be checked elsewhere, this is just for the ast node.
|
||||
with_mock Farmbot.System.Updates, [check_and_download_updates: fn(_) -> :ok end] do
|
||||
ctx = Farmbot.Context.new()
|
||||
ast = %Ast{kind: "check_updates", args: %{package: "farmbot_os"}, body: []}
|
||||
Command.do_command(ast, ctx)
|
||||
assert called Farmbot.System.Updates.check_and_download_updates(ctx)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,70 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.ConfigUpdateTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
|
||||
defp pair(key, val) do
|
||||
%Ast{kind: "pair", args: %{label: key, value: val}, body: []}
|
||||
end
|
||||
|
||||
describe "config_update" do
|
||||
test "makes sure we have serial", %{cs_context: context} do
|
||||
assert Farmbot.Serial.Handler.available?(context) == true
|
||||
end
|
||||
|
||||
test "sets some hardware params", %{cs_context: context} do
|
||||
params = [
|
||||
pair("movement_timeout_x", "1000"),
|
||||
pair("movement_timeout_y", "521")
|
||||
]
|
||||
args = %{package: "arduino_firmware"}
|
||||
Command.config_update(args, params, context)
|
||||
mtx = Farmbot.BotState.get_param context, "movement_timeout_x"
|
||||
mty = Farmbot.BotState.get_param context, "movement_timeout_y"
|
||||
|
||||
assert mtx == 1000
|
||||
assert mty == 521
|
||||
end
|
||||
|
||||
test "doesnt set hardware param values to -1", %{cs_context: context} do
|
||||
old = Farmbot.BotState.get_param context, "movement_timeout_x"
|
||||
params = [
|
||||
pair("movement_timeout_x", "-1")
|
||||
]
|
||||
Command.config_update(%{package: "arduino_firmware"}, params, context)
|
||||
new_mtx = Farmbot.BotState.get_param context, "movement_timeout_x"
|
||||
|
||||
assert new_mtx != -1
|
||||
assert new_mtx == old
|
||||
end
|
||||
|
||||
test "wont put garbage in the state", %{cs_context: context} do
|
||||
params = [ pair("some_garbage", "9001") ]
|
||||
# ITS OVER NINE THOUSAND!!!
|
||||
|
||||
assert_raise RuntimeError, fn ->
|
||||
Command.config_update(%{package: "arduino_firmware"}, params, context)
|
||||
end
|
||||
|
||||
conf = Farmbot.BotState.get_param context, "some_garbage"
|
||||
assert is_nil(conf)
|
||||
end
|
||||
|
||||
test "sets some os params", %{cs_context: context} do
|
||||
params = [
|
||||
pair("steps_per_mm_x", 999),
|
||||
pair("steps_per_mm_y", 40),
|
||||
]
|
||||
Command.config_update(%{package: "farmbot_os"}, params, context)
|
||||
|
||||
spmx = Farmbot.BotState.get_config(context, :steps_per_mm_x)
|
||||
spmy = Farmbot.BotState.get_config(context, :steps_per_mm_y)
|
||||
|
||||
assert spmx == 999
|
||||
assert spmy == 40
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.DataUpdateTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
alias Farmbot.Database, as: DB
|
||||
alias DB.Syncable.Point
|
||||
alias Farmbot.Test.Helpers
|
||||
alias Command.DataUpdate
|
||||
|
||||
setup_all do
|
||||
json = Helpers.read_json("points.json") |> Poison.decode!
|
||||
context = Farmbot.Context.new()
|
||||
{:ok, db_pid} = DB.start_link(context, [])
|
||||
context = %{context | database: db_pid}
|
||||
:ok = Helpers.seed_db(context, Point, json)
|
||||
[
|
||||
json: json,
|
||||
cs_context: context
|
||||
]
|
||||
end
|
||||
|
||||
setup context do
|
||||
DB.unset_awaiting(context.cs_context, Point)
|
||||
end
|
||||
|
||||
test "data_updates causes awaiting to be true.", context do
|
||||
ast = ast("add", [pair("points", "*")])
|
||||
|
||||
old = DB.get_awaiting(context.cs_context, Point)
|
||||
refute(old)
|
||||
|
||||
DataUpdate.run(ast.args, ast.body, context.cs_context)
|
||||
|
||||
new = DB.get_awaiting(context.cs_context, Point)
|
||||
assert(new)
|
||||
end
|
||||
|
||||
def ast(verb, pairs) do
|
||||
%Ast{kind: "data_update", args: %{value: verb}, body: pairs}
|
||||
end
|
||||
|
||||
def pair(mod, thing),
|
||||
do: %Ast{kind: "pair", args: %{label: mod, value: thing}, body: []}
|
||||
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.EmergencyLockTest do
|
||||
alias Farmbot.CeleryScript.{Command, Ast, Error}
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
|
||||
describe "emergency_lock" do
|
||||
test "wont lock the bot if its already locked", %{cs_context: context} do
|
||||
# actually lock the bot
|
||||
ast = good_ast()
|
||||
Command.do_command(ast, context)
|
||||
|
||||
serial_state = :sys.get_state(context.serial)
|
||||
assert serial_state.status == :locked
|
||||
|
||||
config_state = :sys.get_state(context.configuration)
|
||||
|
||||
assert config_state.informational_settings.sync_status == :locked
|
||||
assert config_state.informational_settings.locked == true
|
||||
|
||||
assert_raise Error, "Bot is already locked", fn() ->
|
||||
Command.do_command(ast, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp good_ast, do: %Ast{kind: "emergency_lock", args: %{}, body: []}
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.EmergencyUnLockTest do
|
||||
alias Farmbot.CeleryScript.{Command, Ast, Error}
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
|
||||
describe "emergency_unlock" do
|
||||
test "wont lock the bot if its already locked", %{cs_context: context} do
|
||||
# actually lock the bot
|
||||
lock_ast = good_lock_ast()
|
||||
Command.do_command(lock_ast, context)
|
||||
|
||||
serial_state = :sys.get_state(context.serial)
|
||||
assert serial_state.status == :locked
|
||||
|
||||
config_state = :sys.get_state(context.configuration)
|
||||
|
||||
assert config_state.informational_settings.sync_status == :locked
|
||||
assert config_state.informational_settings.locked == true
|
||||
|
||||
unlock_ast = good_unlock_ast()
|
||||
Command.do_command(unlock_ast, context)
|
||||
|
||||
serial_state = :sys.get_state(context.serial)
|
||||
assert serial_state.status == :idle
|
||||
|
||||
config_state = :sys.get_state(context.configuration)
|
||||
|
||||
assert config_state.informational_settings.sync_status == :sync_now
|
||||
assert config_state.informational_settings.locked == false
|
||||
|
||||
assert_raise Error, "Bot is not locked", fn() ->
|
||||
Command.do_command(unlock_ast, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp good_lock_ast, do: %Ast{kind: "emergency_lock", args: %{}, body: []}
|
||||
defp good_unlock_ast, do: %Ast{kind: "emergency_unlock", args: %{}, body: []}
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.ExplanationTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.CeleryScript.{Ast, Command}
|
||||
alias Command.Explanation
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
[cs_context: ctx]
|
||||
end
|
||||
|
||||
test "pushes explanation onto the data_stack", %{cs_context: ctx} do
|
||||
assert is_map(ctx)
|
||||
msg = "the cat spilled the coffee"
|
||||
ast = %Ast{kind: "explanation", args: %{message: msg}, body: []}
|
||||
|
||||
next_context = Explanation.run(ast.args, ast.body, ctx)
|
||||
assert is_map(next_context)
|
||||
|
||||
assert Enum.count(next_context.data_stack) == (Enum.count(ctx.data_stack) + 1)
|
||||
|
||||
{results, next_context2} = Context.pop_data(next_context)
|
||||
|
||||
assert is_map(next_context2)
|
||||
assert Enum.count(next_context2.data_stack) == (Enum.count(next_context.data_stack) - 1)
|
||||
assert is_map(results)
|
||||
assert results.kind == ast.kind
|
||||
assert results.args == ast.args
|
||||
end
|
||||
|
||||
end
|
|
@ -1,57 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.HomeTest do
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
|
||||
|
||||
alias Farmbot.CeleryScript.Command
|
||||
|
||||
describe "home" do
|
||||
test "makes sure we have serial", %{cs_context: context} do
|
||||
assert Farmbot.Serial.Handler.available?(context) == true
|
||||
end
|
||||
|
||||
test "homes all axises", %{cs_context: context} do
|
||||
Command.home(%{axis: "all"}, [], context)
|
||||
Process.sleep(500)
|
||||
[x, y, z] = Farmbot.BotState.get_current_pos(context)
|
||||
assert x == 0
|
||||
assert y == 0
|
||||
assert z == 0
|
||||
Process.sleep(500)
|
||||
end
|
||||
|
||||
test "homes x", %{cs_context: context} do
|
||||
[_, _, _] = Farmbot.BotState.set_pos(context, 0,0,0)
|
||||
[_x, y, z] = Farmbot.BotState.get_current_pos(context)
|
||||
Command.home(%{axis: "x"}, [], context)
|
||||
Process.sleep(500)
|
||||
[new_x, new_y, new_z] = Farmbot.BotState.get_current_pos(context)
|
||||
assert new_x == 0
|
||||
assert y == new_y
|
||||
assert z == new_z
|
||||
end
|
||||
|
||||
test "homes y", %{cs_context: context} do
|
||||
[_, _, _] = Farmbot.BotState.set_pos(context, 0,0,0)
|
||||
[x, _y, z] = Farmbot.BotState.get_current_pos(context)
|
||||
Command.home(%{axis: "y"}, [], context)
|
||||
Process.sleep(500)
|
||||
[new_x, new_y, new_z] = Farmbot.BotState.get_current_pos(context)
|
||||
assert x == new_x
|
||||
assert new_y == 0
|
||||
assert z == new_z
|
||||
end
|
||||
|
||||
test "homes z", %{cs_context: context} do
|
||||
[_, _, _] = Farmbot.BotState.set_pos(context, 0,0,0)
|
||||
[x, y, _z] = Farmbot.BotState.get_current_pos(context)
|
||||
Command.home(%{axis: "z"}, [], context)
|
||||
Process.sleep(500)
|
||||
[new_x, new_y, new_z] = Farmbot.BotState.get_current_pos(context)
|
||||
assert x == new_x
|
||||
assert y == new_y
|
||||
assert new_z == 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -1,76 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.MoveAbsoluteTest do
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
alias Farmbot.Database, as: DB
|
||||
alias DB.Syncable.Point
|
||||
alias Farmbot.Test.Helpers
|
||||
use Helpers.SerialTemplate, async: false
|
||||
|
||||
describe "move_absolute" do
|
||||
test "makes sure we have serial", %{cs_context: context} do
|
||||
assert Farmbot.Serial.Handler.available?(context) == true
|
||||
end
|
||||
|
||||
test "moves to a location", %{cs_context: context} do
|
||||
[_curx, _cury, _curz] = Farmbot.BotState.get_current_pos(context)
|
||||
location = %Ast{kind: "coordinate", args: %{x: 1000, y: 0, z: 0}, body: []}
|
||||
offset = %Ast{kind: "coordinate", args: %{x: 0, y: 0, z: 0}, body: []}
|
||||
Command.move_absolute(%{speed: 8000, offset: offset, location: location}, [], context)
|
||||
Process.sleep(100) # wait for serial to catch up
|
||||
[newx, _newy, _newz] = Farmbot.BotState.get_current_pos(context)
|
||||
assert newx == 1000
|
||||
end
|
||||
|
||||
test "moves to a location defered by an offset", %{cs_context: context} do
|
||||
location = %Ast{kind: "coordinate", args: %{x: 1000, y: 0, z: 0}, body: []}
|
||||
offset = %Ast{kind: "coordinate", args: %{x: 500, y: 0, z: 0}, body: []}
|
||||
Command.move_absolute(%{speed: 8000, offset: offset, location: location}, [], context)
|
||||
Process.sleep(100) # wait for serial to catch up
|
||||
[newx, newy, newz] = Farmbot.BotState.get_current_pos(context)
|
||||
assert newx == 1500
|
||||
assert newy == 0
|
||||
assert newz == 0
|
||||
end
|
||||
|
||||
test "moves to a bad *plant* location", %{cs_context: context} do
|
||||
[_curx, _cury, _curz] = Farmbot.BotState.get_current_pos(context)
|
||||
location = %Ast{kind: "point",
|
||||
args: %{pointer_type: "Plant", pointer_id: 123},
|
||||
body: []}
|
||||
offset = %Ast{kind: "coordinate",
|
||||
args: %{x: 0, y: 0, z: 0},
|
||||
body: []}
|
||||
args = %{speed: 8000,
|
||||
offset: offset,
|
||||
location: location}
|
||||
assert_raise Farmbot.Database.Selectors.Error, "does not exist.", fn ->
|
||||
Command.move_absolute(args, [], context)
|
||||
end
|
||||
end
|
||||
|
||||
# this test is broke for some reason
|
||||
# test "moves to a good plant", %{cs_context: context} do
|
||||
# Farmbot.BotState.set_pos(context, 0, 0, 0)
|
||||
# json = Helpers.read_json("points.json") |> Poison.decode!
|
||||
# {:ok, db_pid} = DB.start_link(context, [])
|
||||
# context = %{context | database: db_pid}
|
||||
# :ok = Helpers.seed_db(context, Point, json)
|
||||
# item = List.first(json)
|
||||
#
|
||||
# type = item["pointer_type"]
|
||||
# id = item["id"]
|
||||
#
|
||||
# location = %Ast{kind: "point", args: %{pointer_type: type, pointer_id: id}, body: []}
|
||||
# offset = %Ast{kind: "coordinate", args: %{x: 0, y: 0, z: 0}, body: []}
|
||||
# args = %{speed: 8000, offset: offset, location: location}
|
||||
# new_context = Command.move_absolute(args, [], context)
|
||||
#
|
||||
# Process.sleep(1000) # wait for serial to catch up
|
||||
# [x, y, z] = Farmbot.BotState.get_current_pos(new_context)
|
||||
# assert x == item["x"]
|
||||
# assert y == item["y"]
|
||||
# assert z == item["z"]
|
||||
#
|
||||
# end
|
||||
end
|
||||
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.MoveRelativeTest do
|
||||
alias Farmbot.CeleryScript.Command
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
require IEx
|
||||
|
||||
describe "move_absolute" do
|
||||
test "makes sure we have serial", %{cs_context: context} do
|
||||
assert Farmbot.Serial.Handler.available?(context) == true
|
||||
end
|
||||
|
||||
test "moves to a location", %{cs_context: context} do
|
||||
[_, _, _] = Farmbot.BotState.set_pos(context, 500,0,0)
|
||||
[oldx, oldy, oldz] = Farmbot.BotState.get_current_pos(context)
|
||||
|
||||
Command.move_relative(%{speed: 800, x: 100, y: 0, z: 0}, [], context)
|
||||
Process.sleep(600)
|
||||
[newx, newy, newz] = Farmbot.BotState.get_current_pos(context)
|
||||
assert newx == (oldx + 100)
|
||||
assert newy == oldy
|
||||
assert newz == oldz
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.NothingTest do
|
||||
use ExUnit.Case
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
|
||||
test "does nothing" do
|
||||
json = ~s"""
|
||||
{
|
||||
"kind": "nothing",
|
||||
"args": {}
|
||||
}
|
||||
"""
|
||||
ast = json |> Poison.decode!() |> Ast.parse
|
||||
context = Command.do_command(ast, Farmbot.Context.new())
|
||||
{r, _final_context} = Farmbot.Context.pop_data(context)
|
||||
assert is_map(r)
|
||||
assert r.kind == "nothing"
|
||||
end
|
||||
end
|
|
@ -1,33 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.PairTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.CeleryScript.{Ast, Command}
|
||||
alias Command.Pair
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
[cs_context: ctx]
|
||||
end
|
||||
|
||||
test "pushes a pair onto the data_stack", %{cs_context: ctx} do
|
||||
assert is_map(ctx)
|
||||
label = "the_answer_to_the_world"
|
||||
value = 42
|
||||
|
||||
ast = %Ast{kind: "pair", args: %{label: label, value: value}, body: []}
|
||||
|
||||
next_context = Pair.run(ast.args, ast.body, ctx)
|
||||
assert is_map(next_context)
|
||||
|
||||
assert Enum.count(next_context.data_stack) == (Enum.count(ctx.data_stack) + 1)
|
||||
|
||||
{results, next_context2} = Context.pop_data(next_context)
|
||||
|
||||
assert is_map(next_context2)
|
||||
assert Enum.count(next_context2.data_stack) == (Enum.count(next_context.data_stack) - 1)
|
||||
assert is_map(results)
|
||||
assert results.kind == ast.kind
|
||||
assert results.args == ast.args
|
||||
end
|
||||
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.PowerOffTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
|
||||
test "powers off the bot" do
|
||||
json = ~s"""
|
||||
{
|
||||
"kind": "power_off",
|
||||
"args": {}
|
||||
}
|
||||
"""
|
||||
ast = json |> Poison.decode!() |> Ast.parse
|
||||
new_context = Command.do_command(ast, Farmbot.Context.new())
|
||||
assert new_context
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.ReadAllParamsTest do
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
|
||||
|
||||
alias Farmbot.CeleryScript.Command
|
||||
|
||||
test "makes sure we have serial", %{cs_context: context} do
|
||||
assert Farmbot.Serial.Handler.available?(context) == true
|
||||
end
|
||||
|
||||
test "reads all params", %{cs_context: context} do
|
||||
# old = Farmbot.BotState.get_all_mcu_params
|
||||
Command.read_all_params(%{}, [], context)
|
||||
Process.sleep(100)
|
||||
new = Farmbot.BotState.get_all_mcu_params(context)
|
||||
assert is_map(new)
|
||||
assert !Enum.empty?(new)
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.RebootTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
|
||||
test "reboots the bot" do
|
||||
json = ~s"""
|
||||
{
|
||||
"kind": "reboot",
|
||||
"args": {}
|
||||
}
|
||||
"""
|
||||
ast = json |> Poison.decode!() |> Ast.parse
|
||||
new_context = Command.do_command(ast, Farmbot.Context.new())
|
||||
assert new_context
|
||||
end
|
||||
end
|
|
@ -1,40 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.RpcErrorTest do
|
||||
use ExUnit.Case
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
|
||||
setup_all do
|
||||
[cs_context: Farmbot.Context.new()]
|
||||
end
|
||||
|
||||
test "rpc ok", %{cs_context: context} do
|
||||
id = "random thing that should be a uuid"
|
||||
error_message = "the world exploded!"
|
||||
error_json = ~s"""
|
||||
{
|
||||
"kind": "explanation",
|
||||
"args": {"message": "#{error_message}"}
|
||||
}
|
||||
"""
|
||||
json = ~s"""
|
||||
{
|
||||
"kind": "rpc_error",
|
||||
"args": {"label": "#{id}"},
|
||||
"body": [#{error_json}]
|
||||
}
|
||||
"""
|
||||
ast = json |> Poison.decode! |> Ast.parse
|
||||
next_context = Command.do_command(ast, context)
|
||||
|
||||
assert is_map(next_context)
|
||||
assert next_context.__struct__ == Farmbot.Context
|
||||
|
||||
{resp, _final_context} = Farmbot.Context.pop_data(next_context)
|
||||
|
||||
assert resp.kind == "rpc_error"
|
||||
assert resp.args.label == id
|
||||
|
||||
[message] = resp.body
|
||||
assert message.kind == "explanation"
|
||||
assert message.args.message == error_message
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.RpcOkTest do
|
||||
use ExUnit.Case
|
||||
alias Farmbot.CeleryScript.{Command, Ast}
|
||||
|
||||
setup_all do
|
||||
[cs_context: Farmbot.Context.new()]
|
||||
end
|
||||
|
||||
test "rpc ok", %{cs_context: context} do
|
||||
id = "random thing that should be a uuid"
|
||||
json = ~s"""
|
||||
{
|
||||
"kind": "rpc_ok",
|
||||
"args": {"label": "#{id}"}
|
||||
}
|
||||
"""
|
||||
ast = json |> Poison.decode! |> Ast.parse
|
||||
assert is_map(ast)
|
||||
|
||||
next_context = Command.do_command(ast, context)
|
||||
|
||||
assert is_map(next_context)
|
||||
assert next_context.__struct__ == Farmbot.Context
|
||||
|
||||
{resp, _final_context} = Farmbot.Context.pop_data(next_context)
|
||||
assert resp.kind == "rpc_ok"
|
||||
assert resp.args.label == id
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.Command.SyncTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.{Context, Database}
|
||||
alias Farmbot.CeleryScript.Command.Sync
|
||||
alias Farmbot.Test.Helpers
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
{:ok, db} = Database.start_link(ctx, [])
|
||||
# {:ok, auth} = Farmbot.Auth.start_link(ctx, [])
|
||||
ctx = %{ctx | database: db}
|
||||
[cs_context: Helpers.login(ctx)]
|
||||
end
|
||||
#
|
||||
# test "syncs the bot", %{cs_context: ctx} do
|
||||
# db = ctx.database
|
||||
# :ok = Database.flush(ctx)
|
||||
#
|
||||
# use_cassette "sync/corner_case" do
|
||||
# before_state = :sys.get_state(db)
|
||||
# before_count = Enum.count(before_state.all)
|
||||
#
|
||||
# Sync.run(%{}, [], ctx)
|
||||
# after_state = :sys.get_state(db)
|
||||
#
|
||||
# after_count = Enum.count(after_state.all)
|
||||
# assert(before_count < after_count)
|
||||
# end
|
||||
#
|
||||
# end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
defmodule SomeSupervisorProcess do
|
||||
use Farmbot.Context.Supervisor
|
||||
end
|
||||
|
||||
defmodule Farmbot.Context.SupervisorTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
[cs_context: ctx]
|
||||
end
|
||||
|
||||
test "Builds a context tracker", %{cs_context: ctx} do
|
||||
{:ok, pid} = SomeSupervisorProcess.start_link(ctx, [])
|
||||
assert is_pid(pid)
|
||||
assert Process.alive?(pid)
|
||||
end
|
||||
|
||||
test "Does gud pattern matching", %{cs_context: ctx} do
|
||||
assert_raise FunctionClauseError, fn() ->
|
||||
not_quite_context = Map.from_struct(ctx)
|
||||
SomeSupervisorProcess.start_link(not_quite_context, [])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
defmodule Farmbot.Context.TrackerTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.Context.Tracker
|
||||
|
||||
test "builds and tracks a context" do
|
||||
context = Context.new()
|
||||
random_key = Map.from_struct |> Map.delete(:data_stack) |> Map.keys |> Enum.random
|
||||
context = %{ context | random_key => :some_cool_value}
|
||||
{:ok, tracker} = Tracker.start_link(context, [])
|
||||
|
||||
assert is_pid(tracker)
|
||||
assert is_map(context)
|
||||
assert is_atom(random_key)
|
||||
|
||||
ctx = Tracker.get_context(tracker)
|
||||
assert is_map(ctx)
|
||||
assert Map.get(ctx, random_key) == :some_cool_value
|
||||
assert ctx == context
|
||||
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
defmodule SomeWorkerProcess do
|
||||
use Farmbot.Context.Worker
|
||||
end
|
||||
|
||||
defmodule SomeOtherProcess do
|
||||
use Farmbot.Context.Worker
|
||||
|
||||
def init(ctx) do
|
||||
{:ok, %{context: ctx, blah: :important_state} }
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Farmbot.Context.WorkerTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
[cs_context: ctx]
|
||||
end
|
||||
|
||||
test "Builds a context tracker", %{cs_context: ctx} do
|
||||
{:ok, pid} = SomeWorkerProcess.start_link(ctx, [])
|
||||
assert is_pid(pid)
|
||||
state = :sys.get_state(pid)
|
||||
assert state.context == ctx
|
||||
end
|
||||
|
||||
test "Does gud pattern matching", %{cs_context: ctx} do
|
||||
assert_raise FunctionClauseError, fn() ->
|
||||
not_quite_context = Map.from_struct(ctx)
|
||||
SomeWorkerProcess.start_link(not_quite_context, [])
|
||||
end
|
||||
end
|
||||
|
||||
test "allows overwriting `init/1` function", %{cs_context: ctx} do
|
||||
{:ok, pid} = SomeOtherProcess.start_link(ctx, [])
|
||||
assert is_pid(pid)
|
||||
|
||||
state = :sys.get_state(pid)
|
||||
assert state.blah == :important_state
|
||||
end
|
||||
end
|
|
@ -1,72 +0,0 @@
|
|||
defmodule Farmbot.ContextTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.Context
|
||||
alias Farmbot.CeleryScript.Ast
|
||||
|
||||
test "creates a new context" do
|
||||
ctx = Context.new()
|
||||
assert is_map(ctx)
|
||||
assert Map.has_key?(ctx, :data_stack)
|
||||
end
|
||||
|
||||
test "pushes data onto the `data_stack`" do
|
||||
ctx = Context.new()
|
||||
ast = %Ast{kind: "solve_rubiks_cube", args: %{color: "red"}, body: []}
|
||||
next = Context.push_data(ctx, ast)
|
||||
assert is_map(next)
|
||||
assert is_list(next.data_stack)
|
||||
|
||||
assert Enum.count(next.data_stack) == (Enum.count(ctx.data_stack ) + 1)
|
||||
end
|
||||
|
||||
test "Pops data from the `data_stack`" do
|
||||
ctx = Context.new()
|
||||
ast_a = %Ast{kind: "play_video_games", args: %{shape: "blue"}, body: []}
|
||||
ast_b = %Ast{kind: "paint_house", args: %{}, body: []}
|
||||
ctx = Context.push_data(ctx, ast_a) |> Context.push_data(ast_b)
|
||||
|
||||
{item_b, new_context_1} = Context.pop_data(ctx)
|
||||
assert item_b == ast_b
|
||||
|
||||
assert Enum.count(new_context_1.data_stack) == 1
|
||||
|
||||
{item_a, new_context_2} = Context.pop_data(new_context_1)
|
||||
assert item_a == ast_a
|
||||
|
||||
assert Enum.count(new_context_2.data_stack) == 0
|
||||
end
|
||||
|
||||
test "will not push data onto the data_stack that isnt an Ast Node" do
|
||||
ctx = Context.new()
|
||||
assert_raise(FunctionClauseError, fn() ->
|
||||
almost_ast = %{kind: "some_kind", args: %{tape_player: true}, body: []}
|
||||
Context.push_data(ctx, almost_ast)
|
||||
end)
|
||||
end
|
||||
|
||||
test "errors if nothing on the data stack" do
|
||||
ctx = Context.new()
|
||||
assert ctx.data_stack == []
|
||||
assert_raise MatchError, fn() ->
|
||||
{_item, _next} = Context.pop_data(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
test "only accepts tagged structs as a context" do
|
||||
real_ast = %Ast{kind: "this_is_valid", args: %{}, body: []}
|
||||
almost_context = Context.new()
|
||||
|> Context.push_data(real_ast)
|
||||
|> Map.from_struct
|
||||
|
||||
assert is_map(almost_context)
|
||||
refute Map.has_key?(almost_context, :__struct__)
|
||||
|
||||
assert_raise FunctionClauseError, fn() ->
|
||||
Context.push_data(almost_context, real_ast)
|
||||
end
|
||||
|
||||
assert_raise FunctionClauseError, fn() ->
|
||||
Context.pop_data(almost_context)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,198 +0,0 @@
|
|||
defmodule Farmbot.DatabaseTest do
|
||||
alias Farmbot.Test.Helpers
|
||||
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.Database, as: DB
|
||||
alias Farmbot.Context
|
||||
alias DB.Syncable.Point
|
||||
alias Farmbot.Test.Helpers
|
||||
import Helpers, only: [tag_item: 2]
|
||||
|
||||
setup_all do
|
||||
ctx = Context.new()
|
||||
{:ok, db} = DB.start_link(ctx, [])
|
||||
context = %{ctx | database: db}
|
||||
[cs_context: Helpers.login(context)]
|
||||
end
|
||||
|
||||
# HTTP STUB IS R BROKE
|
||||
# test "sync" do
|
||||
# ctx = Context.new()
|
||||
# {:ok, db} = DB.start_link(ctx, [])
|
||||
# context = %{ctx | database: db}
|
||||
# :ok = DB.flush(context)
|
||||
#
|
||||
# use_cassette "sync/corner_case" do
|
||||
# before_state = :sys.get_state(db)
|
||||
# before_count = Enum.count(before_state.all)
|
||||
#
|
||||
# DB.sync(context)
|
||||
#
|
||||
# after_state = :sys.get_state(db)
|
||||
# after_count = Enum.count(after_state.all)
|
||||
# assert(before_count < after_count)
|
||||
# end
|
||||
# end
|
||||
|
||||
|
||||
test "adds a record to the local db", %{cs_context: ctx} do
|
||||
# modulename = Enum.random(DB.all_syncable_modules())
|
||||
modulename = Point
|
||||
plural = modulename.plural_url()
|
||||
points_json = File.read!("fixture/api_fixture/points.json")
|
||||
points = Poison.decode!(points_json) |> Poison.decode!
|
||||
|
||||
old = DB.get_all(ctx, modulename)
|
||||
|
||||
tagged = Enum.map(points, fn(item) ->
|
||||
thing = tag_item(item, modulename)
|
||||
assert(thing.__struct__ == modulename)
|
||||
assert(is_number(thing.id))
|
||||
thing
|
||||
end)
|
||||
|
||||
:ok = DB.commit_records(tagged, ctx, modulename)
|
||||
|
||||
new = DB.get_all(ctx, modulename)
|
||||
assert Enum.count(new) > Enum.count(old)
|
||||
end
|
||||
|
||||
test "wont commit errornous things to db", %{cs_context: ctx} do
|
||||
item = "random_not_json: {}, this isnt formatted_properly!"
|
||||
mod = Enum.random(DB.all_syncable_modules())
|
||||
error = Poison.decode(item)
|
||||
old = DB.get_all(ctx, mod)
|
||||
|
||||
DB.commit_records(error, ctx, mod)
|
||||
|
||||
new = DB.get_all(ctx, mod)
|
||||
assert Enum.count(new) == Enum.count(old)
|
||||
end
|
||||
|
||||
test "gets an item out of the database", %{cs_context: ctx} do
|
||||
modulename = Point
|
||||
plural = modulename.plural_url()
|
||||
points_json = File.read!("fixture/api_fixture/points.json")
|
||||
points = Poison.decode!(points_json) |> Poison.decode!
|
||||
random_item = Enum.random(points) |> tag_item(modulename)
|
||||
|
||||
id = random_item.id
|
||||
|
||||
:ok = DB.commit_records(random_item, ctx, modulename)
|
||||
item = DB.get_by_id(ctx, modulename, id)
|
||||
assert !is_nil(item)
|
||||
assert item.body == random_item
|
||||
end
|
||||
|
||||
test "updates an old item", %{cs_context: ctx} do
|
||||
modulename = Point
|
||||
plural = modulename.plural_url()
|
||||
points_json = File.read!("fixture/api_fixture/points.json")
|
||||
points = Poison.decode!(points_json) |> Poison.decode!
|
||||
random_item = Enum.random(points) |> tag_item(modulename)
|
||||
|
||||
id = random_item.id
|
||||
|
||||
:ok = DB.commit_records(random_item, ctx, modulename)
|
||||
updated = %{random_item | name: "hurdur"}
|
||||
|
||||
:ok = DB.commit_records(updated, ctx, modulename)
|
||||
|
||||
item = DB.get_by_id(ctx, modulename, id)
|
||||
|
||||
assert item.body == updated
|
||||
end
|
||||
|
||||
test "toggles awaiting state for resources", %{cs_context: ctx} do
|
||||
DB.set_awaiting(ctx, Point, :remove, 1)
|
||||
assert(DB.get_awaiting(ctx, Point))
|
||||
|
||||
DB.unset_awaiting(ctx, Point)
|
||||
refute(DB.get_awaiting(ctx, Point))
|
||||
|
||||
DB.set_awaiting(ctx, Point, :remove, 1)
|
||||
assert(DB.get_awaiting(ctx, Point))
|
||||
|
||||
DB.unset_awaiting(ctx, Point)
|
||||
refute(DB.get_awaiting(ctx, Point))
|
||||
end
|
||||
|
||||
test "removes resources on set awaiting", %{cs_context: ctx} do
|
||||
DB.set_awaiting(ctx, Point, :remove, "*")
|
||||
assert(DB.get_awaiting(ctx, Point))
|
||||
|
||||
state = :sys.get_state(ctx.database)
|
||||
assert state.by_kind[Point] == []
|
||||
|
||||
in_all? = Enum.any?(state.all, fn(ref) ->
|
||||
match?({Point, _, _}, ref)
|
||||
end)
|
||||
|
||||
refute in_all?
|
||||
|
||||
in_refs? = Enum.any?(state.refs, fn(thing) ->
|
||||
match? {{Point, _, _}, _}, thing
|
||||
end)
|
||||
|
||||
refute in_refs?
|
||||
|
||||
in_by_kind_and_id? = Enum.any?(Map.keys(state.by_kind_and_id), fn(thing) ->
|
||||
match? {Point, _}, thing
|
||||
end)
|
||||
|
||||
refute in_by_kind_and_id?
|
||||
end
|
||||
|
||||
test "removes a particular item by id", %{cs_context: ctx} do
|
||||
json = ~w"""
|
||||
{
|
||||
"id": 4999,
|
||||
"created_at": "2017-05-16T16:26:13.261Z",
|
||||
"updated_at": "2017-05-16T16:26:13.261Z",
|
||||
"device_id": 2,
|
||||
"meta": {},
|
||||
"name": "Cabbage 2",
|
||||
"pointer_type": "Plant",
|
||||
"radius": 50,
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"z": 2,
|
||||
"openfarm_slug": "cabbage"
|
||||
}
|
||||
"""
|
||||
|
||||
modulename = Point
|
||||
item = Poison.decode!(json)
|
||||
thing = tag_item(item, modulename)
|
||||
assert(thing.__struct__ == modulename)
|
||||
assert(is_number(thing.id))
|
||||
|
||||
:ok = DB.commit_records([thing], ctx, modulename)
|
||||
|
||||
DB.set_awaiting(ctx, Point, :remove, 4999)
|
||||
assert(DB.get_awaiting(ctx, Point))
|
||||
|
||||
state = :sys.get_state(ctx.database)
|
||||
|
||||
in_all? = Enum.find(state.all, fn({syncable, _, id}) ->
|
||||
(syncable == Point) && (id == 4999)
|
||||
end)
|
||||
refute in_all?
|
||||
|
||||
in_refs? = Enum.any?(state.refs, fn({{syncable, _, id}, _}) ->
|
||||
(syncable == Point) && (id == 4999)
|
||||
end)
|
||||
|
||||
refute in_refs?
|
||||
|
||||
in_by_kind_and_id? = Enum.any?(Map.keys(state.by_kind_and_id), fn({syncable, id}) ->
|
||||
(syncable == Point) && (id == 4999)
|
||||
end)
|
||||
|
||||
refute in_by_kind_and_id?
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -1,82 +0,0 @@
|
|||
alias Farmbot.Database
|
||||
alias Database.Syncable
|
||||
|
||||
alias Syncable.Sequence
|
||||
alias Syncable.Point
|
||||
alias Syncable.Peripheral
|
||||
alias Syncable.Regimen
|
||||
alias Syncable.Tool
|
||||
alias Syncable.FarmEvent
|
||||
# alias Syncable.Device
|
||||
|
||||
alias Farmbot.Context
|
||||
|
||||
defmodule AllSyncablesTestHelper do
|
||||
|
||||
defmacro test_syncable(module, id) do
|
||||
quote do
|
||||
|
||||
defmodule Module.concat(["#{unquote(module)}Test"]) do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
setup_all do
|
||||
context = Context.new()
|
||||
[
|
||||
module: unquote(module),
|
||||
id: unquote(id),
|
||||
cs_context: Farmbot.Test.Helpers.login(context),
|
||||
]
|
||||
end
|
||||
|
||||
test "ensures proper module sanity", %{module: mod} do
|
||||
assert unquote(module) == mod
|
||||
end
|
||||
|
||||
test "successfully fetches some stuff from the api", %{module: mod, cs_context: ctx} do
|
||||
human_readable_name = Module.split(mod) |> List.last
|
||||
results = mod.fetch(ctx, {__MODULE__, :callback, []})
|
||||
item = Enum.random(results)
|
||||
assert item.__struct__ == mod
|
||||
|
||||
end
|
||||
|
||||
unless unquote(id) == :no_show do
|
||||
test "gets a particular item from the api", %{module: mod, id: id, cs_context: ctx} do
|
||||
human_readable_name = Module.split(mod) |> List.last
|
||||
results = mod.fetch(ctx, id, {__MODULE__, :callback, []})
|
||||
refute is_error?(results)
|
||||
assert results.__struct__ == mod
|
||||
assert results.id == id
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# test "handles errors for stuff from the api", %{module: mod, cs_context: ctx} do
|
||||
# human_readable_name = Module.split(mod) |> List.last
|
||||
# results = mod.fetch(ctx, -1, {__MODULE__, :callback, []})
|
||||
# assert is_error?(results)
|
||||
# end
|
||||
|
||||
|
||||
def callback(results), do: results
|
||||
#
|
||||
# defp is_error?(results)
|
||||
# defp is_error?({:error, _}), do: true
|
||||
defp is_error?(_), do: false
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
defmodule AllSyncablesTest do
|
||||
import AllSyncablesTestHelper
|
||||
test_syncable Sequence, 2
|
||||
test_syncable Point, 71
|
||||
test_syncable Tool, 1
|
||||
test_syncable FarmEvent, :no_show
|
||||
test_syncable Peripheral, :no_show
|
||||
test_syncable Regimen, :no_show
|
||||
# test_syncable Device, -1
|
||||
end
|
|
@ -1,52 +0,0 @@
|
|||
defmodule Database.Syncable.Fake do
|
||||
use Farmbot.Database.Syncable,
|
||||
model: [:foo, :bar],
|
||||
endpoint: {"/fake", "/fakes"}
|
||||
end
|
||||
|
||||
defmodule Farmbot.SyncableTest do
|
||||
alias Database.Syncable.Fake
|
||||
alias Farmbot.Test.Helpers
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Farmbot.Context
|
||||
|
||||
doctest Farmbot.Database.Syncable
|
||||
|
||||
setup_all do
|
||||
context = Context.new()
|
||||
[my_fake: %Fake{},
|
||||
cs_context: Helpers.login(context)]
|
||||
end
|
||||
|
||||
test "defines a syncable", context do
|
||||
assert(is_map(context.my_fake))
|
||||
assert(context.my_fake.__struct__ == Fake)
|
||||
end
|
||||
|
||||
test "singular URLs" do
|
||||
assert(Fake.singular_url == "/fake")
|
||||
end
|
||||
|
||||
test "plural URLs" do
|
||||
assert(Fake.plural_url == "/fakes")
|
||||
end
|
||||
|
||||
def get_all_by_id_callback(result) do
|
||||
result
|
||||
end
|
||||
|
||||
test "fetch all of a resource", %{cs_context: ctx} do
|
||||
results = Fake.fetch(ctx, {__MODULE__, :get_all_by_id_callback, []})
|
||||
item = List.first(results)
|
||||
assert item.__struct__ == Fake
|
||||
assert is_integer(item.id)
|
||||
end
|
||||
|
||||
test "fetch a particular id of a resource", %{cs_context: ctx} do
|
||||
item = Fake.fetch(ctx, 2, {__MODULE__, :get_all_by_id_callback, []})
|
||||
assert item.__struct__ == Fake
|
||||
assert is_integer(item.id)
|
||||
end
|
||||
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
defmodule Farmbot.Context.TrackerTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.Context.Tracker
|
||||
|
||||
test "builds and tracks a context" do
|
||||
context = Context.new()
|
||||
random_key = Map.from_struct |> Map.delete(:data_stack) |> Map.keys |> Enum.random
|
||||
context = %{ context | random_key => :some_cool_value}
|
||||
{:ok, tracker} = Tracker.start_link(context, [])
|
||||
|
||||
assert is_pid(tracker)
|
||||
assert is_map(context)
|
||||
assert is_atom(random_key)
|
||||
|
||||
ctx = Tracker.get_context(tracker)
|
||||
assert is_map(ctx)
|
||||
assert Map.get(ctx, random_key) == :some_cool_value
|
||||
assert ctx == context
|
||||
|
||||
end
|
||||
end
|
|
@ -1,53 +0,0 @@
|
|||
defmodule Farmbot.EasterEggsTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
setup_all do
|
||||
write_me = test_json() |> Poison.encode!
|
||||
File.write!("/tmp/test.json", write_me)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "starts the server with a path to a file" do
|
||||
path = "/tmp/test.json"
|
||||
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_1}, {:path, path})
|
||||
assert is_pid(pid) == true
|
||||
end
|
||||
|
||||
test "starts the server with a json object" do
|
||||
json = test_json_with_strings()
|
||||
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_2}, {:json, json})
|
||||
assert is_pid(pid) == true
|
||||
end
|
||||
|
||||
test "adds a new json to the state" do
|
||||
path = "/tmp/test.json"
|
||||
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_3}, {:path, path})
|
||||
assert is_pid(pid) == true
|
||||
state1 = GenServer.call(pid, :state)
|
||||
assert state1 == %{nouns: %{}, verbs: []}
|
||||
|
||||
new_json = %{"nouns" => [%{"somehting" => "heyo"}], "verbs" => []}
|
||||
Farmbot.EasterEggs.load_json(new_json, pid)
|
||||
state2 = GenServer.call(pid, :state)
|
||||
assert state2 == %{nouns: %{somehting: "heyo"}, verbs: []}
|
||||
end
|
||||
|
||||
test "logs a thing" do
|
||||
path = "/tmp/test.json"
|
||||
{:ok, pid} = Farmbot.EasterEggs.start_link({:name, :test_4}, {:path, path})
|
||||
assert is_pid(pid) == true
|
||||
state1 = GenServer.call(pid, :state)
|
||||
assert state1 == %{nouns: %{}, verbs: []}
|
||||
|
||||
GenServer.cast pid, "hey this will get logged but logger is disabled hur dur dur"
|
||||
#??? i cant actually test that?
|
||||
end
|
||||
|
||||
def test_json do
|
||||
%{nouns: [], verbs: []}
|
||||
end
|
||||
|
||||
def test_json_with_strings do
|
||||
%{"nouns" => [], "verbs" => []}
|
||||
end
|
||||
end
|
|
@ -1,46 +0,0 @@
|
|||
defmodule Farmbot.Farmware.RuntimeTest do
|
||||
@moduledoc false
|
||||
alias Farmbot.{Farmware, Context, CeleryScript}
|
||||
alias Farmbot.Farmware.Runtime
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
setup_all do
|
||||
context = Context.new()
|
||||
new_context = Farmbot.Test.Helpers.Context.replace_http(context)
|
||||
{:ok, auth} = Farmbot.Auth.start_link(new_context, [])
|
||||
new_context1 = %{new_context | auth: auth}
|
||||
Farmbot.Auth.try_log_in!(new_context1.auth)
|
||||
[cs_context: new_context1]
|
||||
end
|
||||
|
||||
test "Runs a farmware", %{cs_context: ctx} do
|
||||
nothing_node = %CeleryScript.Ast{
|
||||
kind: "nothing",
|
||||
args: %{},
|
||||
body: []
|
||||
}
|
||||
json_stuff = Poison.encode!(nothing_node)
|
||||
|
||||
fake_fw = %Farmware{
|
||||
path: "/tmp/",
|
||||
executable: "bash",
|
||||
uuid: UUID.uuid1(),
|
||||
name: "spectrometer",
|
||||
url: "",
|
||||
args: ["-c", "echo $BEGIN_CELERYSCRIPT #{inspect json_stuff}"],
|
||||
meta: %{
|
||||
min_os_version_major: "1" ,
|
||||
description: "This is a fixture farmware for tests.",
|
||||
language: "python",
|
||||
version: "1.0.0",
|
||||
author: "BLAH Blah",
|
||||
zip: "hello.zip"
|
||||
}
|
||||
}
|
||||
|
||||
new_context = Runtime.execute(ctx, fake_fw)
|
||||
{nothing, new_context1} = Context.pop_data(new_context)
|
||||
assert nothing == nothing_node
|
||||
assert new_context != new_context1
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
defmodule Farmbot.HTTPTest do
|
||||
alias Farmbot.{HTTP, Context}
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
test "wont upload if an image doesnt exist" do
|
||||
img_path = "#{:code.priv_dir(:farmbot)}/static/fake_farmbot_logo.png"
|
||||
assert_raise(HTTP.Error, "#{img_path} not found",
|
||||
fn -> HTTP.upload_file!(Context.new(), img_path) end)
|
||||
end
|
||||
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
defmodule Farmbot.ImageWatcherTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.Auth
|
||||
alias Farmbot.Context
|
||||
|
||||
setup_all do
|
||||
context = Context.new()
|
||||
Auth.purge_token(context.auth)
|
||||
|
||||
# Makes sure the dirs are empty
|
||||
File.rm_rf "/tmp/images"
|
||||
File.mkdir "/tmp/images"
|
||||
Farmbot.ImageWatcher.force_upload(context)
|
||||
:ok = Auth.interim(context.auth, "admin@admin.com", "password123", "http://localhost:3000")
|
||||
{:ok, token} = Auth.try_log_in(context.auth)
|
||||
%{cs_context: context}
|
||||
end
|
||||
|
||||
# test "uploads an image automagically" do
|
||||
# img_path = "#{:code.priv_dir(:farmbot)}/static/farmbot_logo.png"
|
||||
# use_cassette "good_image_upload" do
|
||||
# File.cp! img_path, "/tmp/images/farmbot_logo.png"
|
||||
# end
|
||||
# end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
# defmodule Sequence.SupervisorTest do
|
||||
# @moduledoc false
|
||||
# use ExUnit.Case, async: true
|
||||
# use Amnesia
|
||||
# alias Sequence.Supervisor, as: S
|
||||
# use Farmbot.Sync.Database
|
||||
#
|
||||
# test "starts and stops sequence" do
|
||||
# {:ok, pid} = S.add_child(sequence(), Timex.now())
|
||||
# S.remove_child(sequence())
|
||||
# state = S.get_state
|
||||
# assert is_pid(pid)
|
||||
# assert state.running == nil
|
||||
# end
|
||||
#
|
||||
# test "starts and queues a sequence" do
|
||||
# sA = sequence()
|
||||
# sB = %{sA | id: 123}
|
||||
# now = Timex.now()
|
||||
# {:ok, respA} = S.add_child(sA, now)
|
||||
# {:ok, respB} = S.add_child(sB, now)
|
||||
#
|
||||
# S.remove_child(sA)
|
||||
# S.remove_child(sB)
|
||||
#
|
||||
# state = S.get_state
|
||||
# assert is_pid(respA)
|
||||
# assert respB == :queued
|
||||
# assert state.running == nil
|
||||
# end
|
||||
#
|
||||
# test "starts, finishes a sequence, then starts the next " do
|
||||
# sA = sequence()
|
||||
# sB = %{sA | id: 123}
|
||||
# now = Timex.now()
|
||||
# {:ok, pidA} = S.add_child(sA, now)
|
||||
# {:ok, :queued} = S.add_child(sB, now)
|
||||
# GenServer.stop(pidA, :normal)
|
||||
# state = S.get_state
|
||||
# running = state.running
|
||||
# {pidB, sequence} = running
|
||||
# assert pidA != pidB
|
||||
# end
|
||||
#
|
||||
# defp sequence do
|
||||
# %Sequence{args: %{"is_outdated" => false,
|
||||
# "version" => 4},
|
||||
# body: [], color: "blue", device_id: nil, id: 186,
|
||||
# kind: "sequence", name: "New Sequence"}
|
||||
# end
|
||||
# end
|
|
@ -1,21 +0,0 @@
|
|||
# defmodule Farmbot.SequenceRunnerTest do
|
||||
# @moduledoc false
|
||||
# use ExUnit.Case, async: true
|
||||
# use Amnesia
|
||||
# use Farmbot.Sync.Database
|
||||
# alias Farmbot.SequenceRunner
|
||||
#
|
||||
# test "runs a sequence" do
|
||||
# seq = sequence()
|
||||
# {:ok, pid} = SequenceRunner.start_link(seq)
|
||||
# assert is_pid(pid)
|
||||
# end
|
||||
#
|
||||
# defp sequence do
|
||||
# %Sequence{args: %{"is_outdated" => false,
|
||||
# "version" => 4},
|
||||
# body: [%{"args" => %{"message" => "Bot is at position {{ x }}, {{ y }}, {{ z }}.",
|
||||
# "message_type" => "success"}, "kind" => "send_message"}], color: "blue", id: 186,
|
||||
# kind: "sequence", name: "errrrp"}
|
||||
# end
|
||||
# end
|
|
@ -1,53 +0,0 @@
|
|||
defmodule Farmbot.Serial.Gcode.ParserTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "Parses report paramater" do
|
||||
bleep = Farmbot.Serial.Gcode.Parser.parse_code("R21 P0 V0 Q5")
|
||||
assert(bleep == {"5", {:report_parameter_value, :param_version, 0}})
|
||||
end
|
||||
|
||||
test "pareses a param numbered string" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_param("13")
|
||||
assert(a == :movement_timeout_z)
|
||||
end
|
||||
|
||||
test "Pareses a param in integer form" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_param(13)
|
||||
assert(a == :movement_timeout_z)
|
||||
end
|
||||
|
||||
test "Parses a param in atom form" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_param(:movement_timeout_z)
|
||||
assert(a == 13)
|
||||
end
|
||||
|
||||
test "Parses a param in string form" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_param("movement_timeout_z")
|
||||
assert(a == 13)
|
||||
end
|
||||
|
||||
test "Parses R31 and R41" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_code("R31 P0 V45 Q10")
|
||||
assert a == {"10", {:report_status_value, 0, 45}}
|
||||
end
|
||||
|
||||
test "parses end stops" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_code("R81 XA1 XB1 YA1 YB1 ZA1 ZB1 Q10")
|
||||
assert a == {"10", {:report_end_stops, 1, 1, 1, 1, 1, 1}}
|
||||
end
|
||||
|
||||
test "parses software version" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_code("R83 version string Q22")
|
||||
assert a == {"22", {:report_software_version, "version string"}}
|
||||
end
|
||||
|
||||
test "doesnt parse unhandled codes" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_code("B100")
|
||||
assert a == {:unhandled_gcode, "B100"}
|
||||
end
|
||||
|
||||
test "parses report position" do
|
||||
a = Farmbot.Serial.Gcode.Parser.parse_code("R82 X1 Y2 Z3 Q10")
|
||||
assert a == {"10", {:report_current_position, 1,2,3}}
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
defmodule Farmbot.Serial.HandlerTest do
|
||||
alias Farmbot.Serial.Handler
|
||||
@moduletag [:farmbot_serial]
|
||||
use Farmbot.Test.Helpers.SerialTemplate, async: false
|
||||
|
||||
describe "does serial tests" do
|
||||
|
||||
test "checks serial availablity", %{cs_context: context} do
|
||||
bool = Handler.available?(context)
|
||||
assert bool == true
|
||||
end
|
||||
|
||||
test "gets the state", %{cs_context: context} do
|
||||
state = :sys.get_state(context.serial)
|
||||
assert is_pid(state.nerves)
|
||||
assert is_binary(state.tty)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
defmodule Farmbot.System.FSTest do
|
||||
use ExUnit.Case
|
||||
alias Farmbot.System.FS
|
||||
|
||||
test "blocks and write a file" do
|
||||
path = FS.path() <> "/test_file"
|
||||
stuff = "HELLO WORLD"
|
||||
FS.transaction fn() ->
|
||||
File.write(path, stuff)
|
||||
end, true
|
||||
{:ok, bin} = File.read(path)
|
||||
assert stuff == bin
|
||||
end
|
||||
|
||||
test "times out" do
|
||||
timeout = 1000
|
||||
r = FS.transaction fn() -> Process.sleep(timeout + 100) end, true, timeout
|
||||
assert is_nil r
|
||||
end
|
||||
|
||||
test "makes coverage magically higher" do
|
||||
state = FS.get_state
|
||||
send FS, :uhhh
|
||||
assert is_list(state)
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
defmodule TokenTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
alias Farmbot.Token
|
||||
|
||||
test "creates a token" do
|
||||
url = Faker.Internet.url
|
||||
bot_name = Faker.Name.title
|
||||
date_thing = 123456
|
||||
email = Faker.Internet.email
|
||||
decoded_json =
|
||||
%{"encoded" => "asdfasdfasdfasdfasdfasdflalalalalas",
|
||||
"unencoded" =>
|
||||
%{"bot" => bot_name,
|
||||
"exp" => date_thing,
|
||||
"fw_update_server" => url,
|
||||
"os_update_server" => url,
|
||||
"iat" => date_thing,
|
||||
"iss" => url,
|
||||
"jti" => "123456",
|
||||
"mqtt" => url,
|
||||
"sub" => email}}
|
||||
|
||||
{:ok, f} = Token.create(decoded_json)
|
||||
une = f.unencoded
|
||||
assert(f.encoded == "asdfasdfasdfasdfasdfasdflalalalalas")
|
||||
assert(une.bot == bot_name)
|
||||
assert une.exp == date_thing
|
||||
assert une.iat == date_thing
|
||||
assert une.fw_update_server == url
|
||||
assert une.os_update_server == url
|
||||
assert une.sub == email
|
||||
end
|
||||
|
||||
test "raises an execption on a bad token" do
|
||||
bad_json =
|
||||
%{"encoded" => "abc",
|
||||
"unencoded" => "arbitrary code injection would be cool."}
|
||||
|
||||
assert_raise RuntimeError, fn ->
|
||||
Token.create!(bad_json)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,42 +0,0 @@
|
|||
defmodule Logger.Backends.FarmbotLoggerTest do
|
||||
use ExUnit.Case, async: false
|
||||
alias Farmbot.{Auth, Context}
|
||||
require Logger
|
||||
alias Farmbot.Test.Helpers
|
||||
|
||||
setup_all do
|
||||
backend = Logger.Backends.FarmbotLogger
|
||||
|
||||
on_exit(fn() ->
|
||||
Logger.remove_backend(backend)
|
||||
end)
|
||||
|
||||
context = Context.new()
|
||||
Logger.flush()
|
||||
{:ok, _pid} = Logger.add_backend(backend)
|
||||
ok = GenEvent.call(Logger, backend, {:context, context})
|
||||
[cs_context: Helpers.login(context)]
|
||||
end
|
||||
|
||||
test "logs fifty messages, then uploads them to the api", %{cs_context: ctx} do
|
||||
Logger.flush
|
||||
for i <- 0..51 do
|
||||
Logger.info "Farmbot can count to: #{i}"
|
||||
end
|
||||
Process.sleep(100)
|
||||
|
||||
r = Farmbot.HTTP.get! ctx, "/api/logs"
|
||||
body = Poison.decode!(r.body)
|
||||
assert Enum.count(body) >= 49
|
||||
end
|
||||
|
||||
test "gets the logger state" do
|
||||
Logger.flush
|
||||
|
||||
Logger.info "hey world"
|
||||
state = GenEvent.call(Logger, Logger.Backends.FarmbotLogger, :get_state)
|
||||
assert is_list(state.logs)
|
||||
[log] = state.logs
|
||||
assert log.message == "hey world"
|
||||
end
|
||||
end
|
|
@ -1,40 +0,0 @@
|
|||
Mix.shell.info [:green, "Compiling test helpers."]
|
||||
|
||||
test_lib_dir = "test/test_helpers"
|
||||
File.ls!(test_lib_dir)
|
||||
|> Enum.filter(fn(filename) -> Path.extname(filename) == ".exs" end)
|
||||
|> Enum.all?(fn(f) ->
|
||||
Mix.shell.info [:green, "Compiling #{f}"]
|
||||
Code.require_file("#{test_lib_dir}/#{f}")
|
||||
end) || Mix.raise "Compile error on helpers."
|
||||
|
||||
Mix.shell.info [:green, "Checking init and stuff"]
|
||||
|
||||
spawn Farmbot.Test.Helpers.Checkup, :checkup, []
|
||||
|
||||
Mix.shell.info [:green, "Starting ExCoveralls"]
|
||||
{:ok, _} = Application.ensure_all_started(:excoveralls)
|
||||
|
||||
# Mix.shell.info [:green, "Starting FarmbotSimulator"]
|
||||
# :ok = Application.ensure_started(:farmbot_simulator)
|
||||
|
||||
Process.sleep(100)
|
||||
|
||||
Mix.shell.info [:green, "deleting config and secret"]
|
||||
File.rm_rf! "/tmp/config.json"
|
||||
File.rm_rf! "/tmp/secret"
|
||||
File.rm_rf! "/tmp/farmware"
|
||||
|
||||
Mix.shell.info [:green, "Setting up faker"]
|
||||
Faker.start
|
||||
|
||||
Mix.shell.info [:green, "Setting up vcr"]
|
||||
ExVCR.Config.cassette_library_dir("fixture/cassettes")
|
||||
|
||||
Mix.shell.info [:green, "removeing logger"]
|
||||
Logger.remove_backend Logger.Backends.FarmbotLogger
|
||||
# Farmbot.DebugLog.filter(:all)
|
||||
|
||||
{:ok, pid} = Farmbot.Test.SerialHelper.start_link()
|
||||
Process.link(pid)
|
||||
ExUnit.start
|
|
@ -1,14 +0,0 @@
|
|||
defmodule Farmbot.Test.Helpers.Checkup do
|
||||
|
||||
defp do_exit do
|
||||
Mix.shell.info([:red, "Farmbot isn't alive. Not testing."])
|
||||
System.halt(255)
|
||||
end
|
||||
|
||||
def checkup do
|
||||
fb_pid = Process.whereis(Farmbot.Supervisor) || do_exit()
|
||||
Process.alive?(fb_pid) || do_exit()
|
||||
Process.sleep(500)
|
||||
checkup()
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
defmodule Farmbot.Test.Helpers.Context do
|
||||
alias Farmbot.Context
|
||||
|
||||
def replace_http(%Context{} = context) do
|
||||
{:ok, pid} = Farmbot.Test.Helpers.HTTP.start_link
|
||||
%{context | http: pid}
|
||||
end
|
||||
end
|
|
@ -1,42 +0,0 @@
|
|||
defmodule Farmbot.Test.Helpers do
|
||||
alias Farmbot.Database, as: DB
|
||||
|
||||
def login(%Farmbot.Context{} = context) do
|
||||
{:ok, auth} = Farmbot.Auth.start_link(context, [])
|
||||
next_context = %{context | auth: auth}
|
||||
final_context = Farmbot.Test.Helpers.Context.replace_http(next_context)
|
||||
Farmbot.Auth.interim(auth, "admin@admin.com", "password123", "http://localhost:3000")
|
||||
Farmbot.Auth.try_log_in!(context.auth)
|
||||
final_context
|
||||
end
|
||||
|
||||
def random_file(dir \\ "fixture/api_fixture"),
|
||||
do: File.ls!(dir) |> Enum.random
|
||||
|
||||
def read_json(:random) do
|
||||
random_file() |> read_json
|
||||
end
|
||||
|
||||
def read_json("/" <> file), do: read_json(file)
|
||||
|
||||
def read_json(file) do
|
||||
"fixture/api_fixture/#{file}"
|
||||
|> File.read!()
|
||||
|> Poison.decode!
|
||||
end
|
||||
|
||||
def seed_db(context, module, json) do
|
||||
tagged = Enum.map(json, fn(item) ->
|
||||
tag_item(item, module)
|
||||
end)
|
||||
:ok = DB.commit_records(tagged, context, module)
|
||||
end
|
||||
|
||||
def tag_item(map, tag) do
|
||||
updated_map =
|
||||
map
|
||||
|> Enum.map(fn({key, val}) -> {String.to_atom(key), val} end)
|
||||
|> Map.new()
|
||||
struct(tag, updated_map)
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
defmodule Farmbot.Test.Helpers.HTTP do
|
||||
use GenServer
|
||||
alias Farmbot.HTTP
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
def handle_call({:request, :get, "http://localhost:3000/api/public_key", "", [], []}, _from, state) do
|
||||
pub_key = File.read!("fixture/api_fixture/public_key")
|
||||
response = %HTTP.Response{status_code: 200, body: pub_key, headers: []}
|
||||
{:reply, {:ok, response}, state}
|
||||
end
|
||||
|
||||
def handle_call({:request, :get, "/api/" <> resource, "", [], []}, _from, state) do
|
||||
resource = String.replace(resource, "/", "_")
|
||||
if "#{resource}.json" in File.ls!("fixture/api_fixture") do
|
||||
json = File.read!("fixture/api_fixture/#{resource}.json") |> Poison.decode!
|
||||
response = %HTTP.Response{status_code: 200, body: json, headers: []}
|
||||
{:reply, {:ok, response}, state}
|
||||
else
|
||||
raise "Could not find: #{resource}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def handle_call({:request, :post, "http://localhost:3000/api/tokens", _, [], []}, _from, state) do
|
||||
token = File.read!("fixture/api_fixture/token.json") |> Poison.decode!
|
||||
response = %HTTP.Response{status_code: 200, body: token, headers: []}
|
||||
{:reply, {:ok, response}, state}
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -1,67 +0,0 @@
|
|||
defmodule Farmbot.Test.SerialHelper do
|
||||
use GenServer
|
||||
alias Farmbot.Context
|
||||
|
||||
def setup_serial do
|
||||
context = Context.new()
|
||||
{ttya, ttyb} = slot = get_slot()
|
||||
{:ok, hand} = Farmbot.Serial.Handler.start_link(context, ttyb, [])
|
||||
{:ok, firm} = FirmwareSimulator.start_link(ttya, [])
|
||||
context = %{context | serial: hand}
|
||||
# IO.puts "claiming slot: #{inspect slot}"
|
||||
{{hand, firm}, slot, context}
|
||||
end
|
||||
|
||||
def teardown_serial({hand, firm}, slot) do
|
||||
# IO.puts "releaseing slot: #{inspect slot}"
|
||||
spawn fn() ->
|
||||
GenServer.stop(hand, :shutdown)
|
||||
GenServer.stop(firm, :shutdown)
|
||||
|
||||
end
|
||||
done_with_slot(slot)
|
||||
end
|
||||
|
||||
def start_link do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
def get_slot do
|
||||
GenServer.call(__MODULE__, :get_slot, :infinity)
|
||||
end
|
||||
|
||||
def done_with_slot(slot) do
|
||||
GenServer.call(__MODULE__, {:done_with_slot, slot})
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
slots = [{"tnt0", "tnt1"}, {"tnt2", "tnt3"}, {"tnt4", "tnt5"}, {"tnt6", "tnt7"}]
|
||||
slot_map = Map.new(slots, fn(slot) -> {slot, nil} end)
|
||||
{:ok, %{slots: slot_map, waiting_for_slots: []}}
|
||||
end
|
||||
|
||||
def handle_call(:get_slot, from, state) do
|
||||
slot = Enum.find_value(state.slots, fn({slot, user}) ->
|
||||
unless user do
|
||||
slot
|
||||
end
|
||||
end)
|
||||
|
||||
if slot do
|
||||
{:reply, slot, %{state | slots: %{state.slots | slot => from}}}
|
||||
else
|
||||
new_waiting = [from | state.waiting_for_slots]
|
||||
{:noreply, %{state | waiting_for_slots: new_waiting}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:done_with_slot, slot}, from, state) do
|
||||
case Enum.reverse state.waiting_for_slots do
|
||||
[next_in_line | rest] ->
|
||||
GenServer.reply(next_in_line, slot)
|
||||
{:reply, :ok, %{state | waiting_for_slots: rest, slots: %{state.slots | slot => from}}}
|
||||
[] ->
|
||||
{:reply, :ok, %{state | slots: %{state.slots | slot => nil}}}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
defmodule Farmbot.Test.Helpers.SerialTemplate do
|
||||
alias Farmbot.Serial.Handler
|
||||
alias Farmbot.Test.SerialHelper
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
defp wait_for_serial(context) do
|
||||
unless Handler.available?(context) do
|
||||
# IO.puts "waiting for serial..."
|
||||
Process.sleep(100)
|
||||
wait_for_serial(context)
|
||||
end
|
||||
end
|
||||
|
||||
setup_all do
|
||||
{{hand, firm}, slot, context} = SerialHelper.setup_serial()
|
||||
wait_for_serial(context)
|
||||
|
||||
on_exit fn() -> SerialHelper.teardown_serial({hand, firm}, slot) end
|
||||
[cs_context: context, serial_handler: hand, firmware_sim: firm]
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue