Merge pull request #1197 from FarmBot/staging

Production release of v9.2.2
qa/why_are_otas_broke v9.2.2
Rick Carlino 2020-04-13 14:20:02 -05:00 committed by GitHub
commit a57f44976b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1402 additions and 904 deletions

View File

@ -201,6 +201,7 @@ jobs:
mix compile mix compile
mix format --check-formatted mix format --check-formatted
mix coveralls.json mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: v14-fbcs-test-dependency-cache-{{ checksum "farmbot_celery_script/mix.lock" }} key: v14-fbcs-test-dependency-cache-{{ checksum "farmbot_celery_script/mix.lock" }}
paths: paths:
@ -235,6 +236,7 @@ jobs:
mix compile mix compile
mix format --check-formatted mix format --check-formatted
mix coveralls.json mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: v14-fbfw-test-dependency-cache-{{ checksum "farmbot_firmware/mix.lock" }} key: v14-fbfw-test-dependency-cache-{{ checksum "farmbot_firmware/mix.lock" }}
paths: paths:
@ -279,6 +281,7 @@ jobs:
mix compile mix compile
mix format --check-formatted mix format --check-formatted
mix coveralls.json --trace mix coveralls.json --trace
bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: v14-fbcore-test-dependency-cache-{{ checksum "farmbot_core/mix.lock" }} key: v14-fbcore-test-dependency-cache-{{ checksum "farmbot_core/mix.lock" }}
paths: paths:
@ -327,6 +330,7 @@ jobs:
mix ecto.create mix ecto.create
mix ecto.migrate mix ecto.migrate
mix coveralls.json mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: v14-fbext-test-dependency-cache-{{ checksum "farmbot_ext/mix.lock" }} key: v14-fbext-test-dependency-cache-{{ checksum "farmbot_ext/mix.lock" }}
paths: paths:
@ -362,6 +366,7 @@ jobs:
mix compile mix compile
mix format --check-formatted mix format --check-formatted
mix coveralls.json mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: v14-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock" }} key: v14-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock" }}
paths: paths:
@ -374,58 +379,6 @@ jobs:
- store_artifacts: - store_artifacts:
path: farmbot_os/cover path: farmbot_os/cover
report_coverage:
<<: *defaults
environment:
MIX_ENV: test
MIX_TARGET: host
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
ELIXIR_VERSION: 1.8.0
steps:
- checkout
- run: git submodule update --init --recursive
- <<: *install_elixir
- <<: *install_hex_archives
- <<: *install_mdl
- restore_cache:
keys:
- v14-fbsupport-test-dependency-cache-{{ checksum "mix.lock" }}
- restore_cache:
keys:
- v14-fbcs-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbcs-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbfw-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbcore-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbext-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbos-coverage-cache-{{ .Branch }}-{{ .Revision }}
- run:
name: Check documentation formatting
command: |
mdl docs/
- run:
name: Report Coverage
working_directory: /nerves/build/
command: |
mix deps.get
mix compile
mix format --check-formatted
mix farmbot.coveralls circle
- save_cache:
key: v14-fbsupport-test-dependency-cache-{{ checksum "mix.lock" }}
paths:
- deps/
- _build
################################################################################ ################################################################################
# target=rpi app_env=prod # # target=rpi app_env=prod #
################################################################################ ################################################################################
@ -719,22 +672,6 @@ workflows:
- beta - beta
- next - next
- /^qa\/.*/ - /^qa\/.*/
- report_coverage:
context: org-global
requires:
- test_farmbot_celery_script
- test_farmbot_firmware
- test_farmbot_core
- test_farmbot_ext
- test_farmbot_os
filters:
branches:
ignore:
- master
- staging
- beta
- next
- /^qa\/.*/
# master branch to staging.farmbot.io # master branch to staging.farmbot.io
nerves_hub_prod_stable_staging: nerves_hub_prod_stable_staging:

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
# 9.2.2
* Fix firmware locking error ("Can't perform X in Y state")
* Removal of dead code / legacy plus numerous unit test additions.
* Added coveralls test coverage reporter
* Unit test additions (+2.7% coverage :tada:)
* Updates to build instructions for third party developers
* Bug fix for criteria-based groups that have only one filter criteria.
* Bug fix for express bots involving timeout during remote firmware flash
* Remove VCR again (for now)
* Increase farmware timeout to 20 minutes (use at own risk)
# 9.2.1 # 9.2.1
* Improve firmware debug messages. * Improve firmware debug messages.

View File

@ -9,9 +9,9 @@ Old versions of FarmBot OS can be found [here](https://github.com/FarmBot/farmbo
--- ---
## Build status ## Build status
| Master Build Status | Staging Build Status | | Master Build Status | Staging Build Status | Test Coverage |
| :---: | :---: | | :---: | :---: | :---: |
| [![Master Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/master.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/master) | [![Staging Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging) | | [![Master Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/master.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/master) | [![Staging Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging) | [![codecov](https://codecov.io/gh/FarmBot/farmbot_os/branch/staging/graph/badge.svg)](https://codecov.io/gh/FarmBot/farmbot_os) |
--- ---

View File

@ -1 +1 @@
9.2.1 9.2.2

View File

@ -6,6 +6,30 @@ This document will act as an index to available documentation.
* [FarmBot Source Code common terms](/docs/glossary.md) * [FarmBot Source Code common terms](/docs/glossary.md)
## Cheat Sheet
**Create a *.fw file from local repo (RPi Zero):**
```sh
NERVES_SYSTEM=farmbot_system_rpi MIX_TARGET=rpi mix deps.get
NERVES_SYSTEM=farmbot_system_rpi MIX_TARGET=rpi mix firmware
sudo fwup farmbot_os/_build/rpi/rpi_dev/nerves/images/farmbot.fw
```
**Create a *.fw file from local repo (RPi v3):**
```sh
NERVES_SYSTEM=farmbot_system_rpi3 MIX_TARGET=rpi3 mix deps.get
NERVES_SYSTEM=farmbot_system_rpi3 MIX_TARGET=rpi3 mix firmware
sudo fwup farmbot_os/_build/rpi3/rpi3_dev/nerves/images/farmbot.fw
```
**Create or Update the Nerves System:**
Please see the official [Nerves documentation on "Nerves Systems"](https://hexdocs.pm/nerves/0.4.0/systems.html).
HINT: You may want to [develop the system locally](https://stackoverflow.com/a/28189056/1064917)
## Hardware specifics ## Hardware specifics
Most FarmBot development/testing is done on a standard desktop PC. Most FarmBot development/testing is done on a standard desktop PC.

View File

@ -20,15 +20,22 @@ string.
```bash ```bash
cd $FARMBOT_OS_ROOT_DIRECTORY cd $FARMBOT_OS_ROOT_DIRECTORY
git checkout staging git checkout staging
git fetch --all && git reset --hard origin/staging
# Ensure you don't accidentally publish local changes
# that have not gone through CI:
git fetch --all
git reset --hard origin/staging
# update the CHANGELOG, but DO NOT put the `rc` # update the CHANGELOG, but DO NOT put the `rc`
# on the semver string. # on the semver string.
$EDITOR CHANGELOG.md $EDITOR CHANGELOG.md
echo 10.5.6-rc30 > VERSION
git add CHANGELOG.md VERSION echo 1.2.3-rc4 > VERSION
git commit -m "Release v10.5.6-rc30"
git tag v$(cat VERSION) git add -A
git push origin staging v$(cat VERSION) git commit -am "Release v10.5.6-rc30"
git tag v1.2.3-rc4
git push origin v1.2.3-rc4
``` ```
or call the helper script: or call the helper script:

View File

@ -25,3 +25,4 @@ farmbot_ng-*.tar
*.sqlite3 *.sqlite3
*.so *.so
*.hex *.hex
*.coverdata

View File

@ -5,10 +5,35 @@ defmodule FarmbotCeleryScript.AST.Factory do
alias FarmbotCeleryScript.AST alias FarmbotCeleryScript.AST
@doc """
Create an empty AST WITH ARG SET TO `nil`.
iex> new()
%FarmbotCeleryScript.AST{
args: nil,
body: [],
comment: nil,
kind: nil,
meta: nil
}
"""
def new do def new do
%AST{body: []} %AST{body: []}
end end
@doc """
Create a new AST to work with. Strings `kind`s are
converted to symbols.
iex> new("foo")
%FarmbotCeleryScript.AST{
args: %{},
body: [],
comment: nil,
kind: :foo,
meta: nil
}
"""
def new(kind, args \\ %{}, body \\ []) do def new(kind, args \\ %{}, body \\ []) do
AST.new(kind, Map.new(args), body) AST.new(kind, Map.new(args), body)
end end
@ -24,59 +49,148 @@ defmodule FarmbotCeleryScript.AST.Factory do
) )
end end
@doc """
iex> (new() |> rpc_request("x") |> set_pin_io_mode(13, 1)).body
[%FarmbotCeleryScript.AST{
kind: :set_pin_io_mode,
args: %{ pin_io_mode: 1, pin_number: 13 },
body: [],
comment: nil,
meta: nil
}]
"""
def set_pin_io_mode(%AST{} = ast, pin_number, pin_io_mode) do def set_pin_io_mode(%AST{} = ast, pin_number, pin_io_mode) do
ast args = %{pin_number: pin_number, pin_io_mode: pin_io_mode}
|> add_body_node( ast |> add_body_node(new(:set_pin_io_mode, args))
new(:set_pin_io_mode, %{pin_number: pin_number, pin_io_mode: pin_io_mode})
)
end end
@doc """
iex> (new() |> rpc_request("x") |> emergency_lock()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :emergency_lock
}]
"""
def emergency_lock(%AST{} = ast) do def emergency_lock(%AST{} = ast) do
ast ast |> add_body_node(new(:emergency_lock))
|> add_body_node(new(:emergency_lock))
end end
@doc """
iex> (new() |> rpc_request("x") |> emergency_unlock()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :emergency_unlock
}]
"""
def emergency_unlock(%AST{} = ast) do def emergency_unlock(%AST{} = ast) do
ast ast |> add_body_node(new(:emergency_unlock))
|> add_body_node(new(:emergency_unlock))
end end
@doc """
iex> (new() |> rpc_request("x") |> read_status()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :read_status
}]
"""
def read_status(%AST{} = ast) do def read_status(%AST{} = ast) do
ast ast |> add_body_node(new(:read_status))
|> add_body_node(new(:read_status))
end end
@doc """
iex> (new() |> rpc_request("x") |> power_off()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :power_off
}]
"""
def power_off(%AST{} = ast) do def power_off(%AST{} = ast) do
ast ast |> add_body_node(new(:power_off))
|> add_body_node(new(:power_off))
end end
@doc """
iex> (new() |> rpc_request("x") |> reboot()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :reboot
}]
"""
def reboot(%AST{} = ast) do def reboot(%AST{} = ast) do
ast ast |> add_body_node(new(:reboot))
|> add_body_node(new(:reboot))
end end
@doc """
iex> (new() |> rpc_request("x") |> sync()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :sync
}]
"""
def sync(%AST{} = ast) do def sync(%AST{} = ast) do
ast ast |> add_body_node(new(:sync))
|> add_body_node(new(:sync))
end end
@doc """
iex> (new() |> rpc_request("x") |> take_photo()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :take_photo
}]
"""
def take_photo(%AST{} = ast) do def take_photo(%AST{} = ast) do
ast ast |> add_body_node(new(:take_photo))
|> add_body_node(new(:take_photo))
end end
@doc """
iex> (new() |> rpc_request("x") |> flash_firmware("arduino")).body
[%FarmbotCeleryScript.AST{
kind: :flash_firmware,
comment: nil,
meta: nil,
args: %{package: "arduino"},
body: [],
}]
"""
def flash_firmware(%AST{} = ast, package) when is_binary(package) do def flash_firmware(%AST{} = ast, package) when is_binary(package) do
ast ast |> add_body_node(new(:flash_firmware, %{package: package}))
|> add_body_node(new(:flash_firmware, %{package: package})) end
@doc """
iex> (new() |> rpc_request("x") |> factory_reset("arduino")).body
[%FarmbotCeleryScript.AST{
kind: :factory_reset,
comment: nil,
meta: nil,
args: %{package: "arduino"},
body: [],
}]
"""
def factory_reset(%AST{} = ast, package) do
ast |> add_body_node(new(:factory_reset, %{package: package}))
end end
def add_body_node(%AST{body: body} = ast, %AST{} = body_node) do def add_body_node(%AST{body: body} = ast, %AST{} = body_node) do
%{ast | body: body ++ [body_node]} %{ast | body: body ++ [body_node]}
end end
def factory_reset(%AST{} = ast, package) do
ast
|> add_body_node(new(:factory_reset, %{package: package}))
end
end end

View File

@ -0,0 +1,4 @@
defmodule FarmbotCeleryScript.AST.FactoryTest do
use ExUnit.Case, async: true
doctest FarmbotCeleryScript.AST.Factory, import: true
end

View File

@ -3,6 +3,7 @@ defmodule FarmbotCeleryScript.SchedulerTest do
use Mimic use Mimic
alias FarmbotCeleryScript.{Scheduler, AST} alias FarmbotCeleryScript.{Scheduler, AST}
alias FarmbotCeleryScript.SysCalls.Stubs alias FarmbotCeleryScript.SysCalls.Stubs
import ExUnit.CaptureLog
setup :set_mimic_global setup :set_mimic_global
setup :verify_on_exit! setup :verify_on_exit!
@ -21,11 +22,14 @@ defmodule FarmbotCeleryScript.SchedulerTest do
|> AST.Factory.read_pin(9, 0) |> AST.Factory.read_pin(9, 0)
scheduled_time = DateTime.utc_now() |> DateTime.add(100, :millisecond) scheduled_time = DateTime.utc_now() |> DateTime.add(100, :millisecond)
# msg = "[info] Next execution is ready for execution: now"
{:ok, _} = Scheduler.schedule(sch, ast, scheduled_time, %{}) {:ok, _} = Scheduler.schedule(sch, ast, scheduled_time, %{})
# Hack to force the scheduler to checkup instead of waiting the normal 15 seconds # Hack to force the scheduler to checkup instead of waiting the normal 15 seconds
send(sch, :checkup) assert capture_log(fn ->
# Sorry. send(sch, :checkup)
Process.sleep(1100) # Sorry.
Process.sleep(1100)
end) =~ "[info] Next execution is ready for execution: now"
end end
end end

View File

@ -5,6 +5,9 @@ defmodule FarmbotCeleryScriptTest do
alias FarmbotCeleryScript.AST alias FarmbotCeleryScript.AST
alias FarmbotCeleryScript.SysCalls.Stubs alias FarmbotCeleryScript.SysCalls.Stubs
import ExUnit.CaptureIO
import ExUnit.CaptureLog
setup :verify_on_exit! setup :verify_on_exit!
test "uses default values when no parameter is found" do test "uses default values when no parameter is found" do
@ -59,8 +62,10 @@ defmodule FarmbotCeleryScriptTest do
:ok :ok
end) end)
result = FarmbotCeleryScript.execute(sequence_ast, me) capture_log(fn ->
assert :ok == result result = FarmbotCeleryScript.execute(sequence_ast, me)
assert :ok == result
end) =~ "[error] CeleryScript syscall stubbed: log"
end end
test "syscall errors" do test "syscall errors" do
@ -93,11 +98,17 @@ defmodule FarmbotCeleryScriptTest do
} }
|> AST.decode() |> AST.decode()
expect(Stubs, :read_pin, fn _, _ -> raise("big oops") end) expect(Stubs, :read_pin, fn _, _ ->
raise("big oops")
end)
assert {:error, "big oops"} == io =
FarmbotCeleryScript.execute(execute_ast, execute_ast) capture_io(:stderr, fn ->
assert {:error, "big oops"} ==
FarmbotCeleryScript.execute(execute_ast, execute_ast)
end)
assert io =~ "CeleryScript Exception"
assert_receive {:step_complete, ^execute_ast, {:error, "big oops"}} assert_receive {:step_complete, ^execute_ast, {:error, "big oops"}}
end end
end end

View File

@ -1,4 +1,5 @@
use Mix.Config use Mix.Config
config :logger, level: :warn
# must be lower than other timers # must be lower than other timers
# To ensure other timers have time to timeout # To ensure other timers have time to timeout
@ -17,3 +18,19 @@ config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 0 firmware_flash_attempt_threshold: 0
if Mix.env() == :test do
config :ex_unit, capture_logs: true
mapper = fn mod -> config :farmbot_core, mod, children: [] end
list = [
FarmbotCore,
FarmbotCore.StorageSupervisor,
FarmbotCore.Asset.Supervisor,
FarmbotCore.BotState.Supervisor,
FarmbotCore.Config.Supervisor,
FarmbotCore.Logger.Supervisor
]
Enum.map(list, mapper)
end

View File

@ -1,6 +1,6 @@
{ {
"coverage_options": { "coverage_options": {
"treat_no_relevant_lines_as_covered": true, "treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 24 "minimum_coverage": 25
} }
} }

View File

@ -14,8 +14,11 @@ defmodule FarmbotCore do
def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__) def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__)
def init([]) do def init([]) do
Supervisor.init(children(), [strategy: :one_for_one])
end
children = [ def children do
default = [
FarmbotCore.Leds, FarmbotCore.Leds,
FarmbotCore.EctoMigrator, FarmbotCore.EctoMigrator,
FarmbotCore.BotState.Supervisor, FarmbotCore.BotState.Supervisor,
@ -27,6 +30,7 @@ defmodule FarmbotCore do
{FarmbotFirmware, transport: FarmbotFirmware.StubTransport, side_effects: FarmbotCore.FirmwareSideEffects}, {FarmbotFirmware, transport: FarmbotFirmware.StubTransport, side_effects: FarmbotCore.FirmwareSideEffects},
FarmbotCeleryScript.Scheduler FarmbotCeleryScript.Scheduler
] ]
Supervisor.init(children, [strategy: :one_for_one]) config = (Application.get_env(:farmbot_ext, __MODULE__) || [])
Keyword.get(config, :children, default)
end end
end end

View File

@ -62,11 +62,6 @@ defmodule FarmbotCore.Asset do
## Begin FarmEvent ## Begin FarmEvent
@doc "Returns all FarmEvents"
def list_farm_events do
Repo.all(FarmEvent)
end
def new_farm_event!(params) do def new_farm_event!(params) do
%FarmEvent{} %FarmEvent{}
|> FarmEvent.changeset(params) |> FarmEvent.changeset(params)
@ -328,8 +323,7 @@ defmodule FarmbotCore.Asset do
# the DB / API. # the DB / API.
sorted = CriteriaRetriever.run(point_group) sorted = CriteriaRetriever.run(point_group)
|> sort_points(sort_by || "xy_ascending") |> sort_points(sort_by || "xy_ascending")
|> Enum.map(&Map.fetch!(&1, :id)) |> Enum.map(fn point -> point.id end)
%{ point_group | point_ids: sorted } %{ point_group | point_ids: sorted }
other -> other ->
# Swallow all other errors # Swallow all other errors
@ -353,7 +347,7 @@ defmodule FarmbotCore.Asset do
|> Repo.update!() |> Repo.update!()
regimen_instances = list_regimen_instances() regimen_instances = list_regimen_instances()
farm_events = list_farm_events() farm_events = Repo.all(FarmEvent)
# check for any matching asset using this point group. # check for any matching asset using this point group.
# This is pretty recursive and probably isn't super great # This is pretty recursive and probably isn't super great

View File

@ -22,7 +22,7 @@ defmodule FarmbotCore.Asset.CriteriaRetriever do
# We will not query any string/numeric fields other than these. # We will not query any string/numeric fields other than these.
# Updating the PointGroup / Point models may require an update # Updating the PointGroup / Point models may require an update
# to these fields. # to these fields.
@numberic_fields ["radius", "x", "y", "z"] @numberic_fields ["radius", "x", "y", "z", "pullout_direction"]
@string_fields ["name", "openfarm_slug", "plant_stage", "pointer_type"] @string_fields ["name", "openfarm_slug", "plant_stage", "pointer_type"]
@doc """ @doc """
@ -49,7 +49,7 @@ defmodule FarmbotCore.Asset.CriteriaRetriever do
needs_meta_filter = Repo.all(dynamic_query) needs_meta_filter = Repo.all(dynamic_query)
# There we go. We have all the matching %Point{}s # There we go. We have all the matching %Point{}s
search_matches = search_meta_fields(pg, needs_meta_filter) search_matches = search_meta_fields(pg, needs_meta_filter)
# ...but there are duplicates. We can remove them via uniq_by: # ...but there are duplicates. We can remove them via uniq_by:
Enum.uniq_by((search_matches ++ always_ok), fn p -> p.id end) Enum.uniq_by((search_matches ++ always_ok), fn p -> p.id end)
end end
@ -114,7 +114,8 @@ defmodule FarmbotCore.Asset.CriteriaRetriever do
def finalize({fragments, criteria}) do def finalize({fragments, criteria}) do
x = Enum.join(fragments, " AND ") x = Enum.join(fragments, " AND ")
sql = "SELECT id FROM points WHERE #{x}" sql = "SELECT id FROM points WHERE #{x}"
{:ok, query} = Repo.query(sql, List.flatten(criteria)) query_params = List.flatten(criteria)
{:ok, query} = Repo.query(sql, query_params)
%Sqlite.DbConnection.Result{ rows: rows } = query %Sqlite.DbConnection.Result{ rows: rows } = query
List.flatten(rows) List.flatten(rows)
end end
@ -136,11 +137,13 @@ defmodule FarmbotCore.Asset.CriteriaRetriever do
if days == 0 do if days == 0 do
{ pg, accum } { pg, accum }
else else
op = day_criteria["op"] || "<" op = day_criteria["op"] || "<"
time = Timex.shift(Timex.now(), days: -1 * days) time = Timex.shift(Timex.now(), days: -1 * days)
{ pg, accum ++ [{"created_at", op, time}] } inverted_op = if op == ">" do "<" else ">" end
{ pg, accum ++ [{"created_at", inverted_op, time}] }
end end
end end
@ -159,14 +162,19 @@ defmodule FarmbotCore.Asset.CriteriaRetriever do
# NOT OK: Repo.query("SELECT foo WHERE bar IN $0", [[1, 2, 3]]) # NOT OK: Repo.query("SELECT foo WHERE bar IN $0", [[1, 2, 3]])
# OK: Repo.query("SELECT foo WHERE bar IN ($0, $1, $2)", [1, 2, 3]) # OK: Repo.query("SELECT foo WHERE bar IN ($0, $1, $2)", [1, 2, 3])
defp stage_3({sql, args}, {full_query, full_args, count0}) when is_list(args) do defp stage_3({sql, args}, {full_query, full_args, count0}) when is_list(args) do
final = count0 + Enum.count(args) - 1 arg_count = Enum.count(args)
final = count0 + (arg_count - 1)
initial_state = {sql, count0} initial_state = {sql, count0}
{next_sql, _} = Enum.reduce(args, initial_state, fn {next_sql, _} =
(_, {sql, ^count0}) -> {sql <> " ($#{count0},", count0+1} if arg_count == 1 do
(_, {sql, ^final}) -> {sql <> " $#{final})", final} {sql <> " ($#{count0})", nil}
(_, {sql, count}) -> {sql <> " $#{count},", count+1} else
end) Enum.reduce(args, initial_state, fn
(_, {sql, ^count0}) -> {sql <> " ($#{count0},", count0+1}
(_, {sql, ^final}) -> {sql <> " $#{final})", final}
(_, {sql, count}) -> {sql <> " $#{count},", count+1}
end)
end
{full_query ++ [next_sql], full_args ++ [args], final + 1} {full_query ++ [next_sql], full_args ++ [args], final + 1}
end end

View File

@ -13,20 +13,21 @@ defmodule FarmbotCore.Asset.Point do
foreign_key: :asset_local_id foreign_key: :asset_local_id
) )
field(:discarded_at, :utc_datetime)
field(:gantry_mounted, :boolean)
field(:meta, :map) field(:meta, :map)
field(:monitor, :boolean, default: true)
field(:name, :string) field(:name, :string)
field(:openfarm_slug, :string) field(:openfarm_slug, :string)
field(:plant_stage, :string) field(:plant_stage, :string)
field(:planted_at, :utc_datetime) field(:planted_at, :utc_datetime)
field(:pointer_type, :string) field(:pointer_type, :string)
field(:pullout_direction, :integer)
field(:radius, :float) field(:radius, :float)
field(:tool_id, :integer)
field(:x, :float) field(:x, :float)
field(:y, :float) field(:y, :float)
field(:z, :float) field(:z, :float)
field(:tool_id, :integer)
field(:discarded_at, :utc_datetime)
field(:gantry_mounted, :boolean)
field(:monitor, :boolean, default: true)
timestamps() timestamps()
end end
@ -42,6 +43,8 @@ defmodule FarmbotCore.Asset.Point do
tool_id: point.tool_id, tool_id: point.tool_id,
discarded_at: point.discarded_at, discarded_at: point.discarded_at,
gantry_mounted: point.gantry_mounted, gantry_mounted: point.gantry_mounted,
openfarm_slug: point.openfarm_slug,
pullout_direction: point.pullout_direction,
x: point.x, x: point.x,
y: point.y, y: point.y,
z: point.z z: point.z
@ -51,22 +54,24 @@ defmodule FarmbotCore.Asset.Point do
def changeset(point, params \\ %{}) do def changeset(point, params \\ %{}) do
point point
|> cast(params, [ |> cast(params, [
:created_at,
:discarded_at,
:gantry_mounted,
:id, :id,
:meta, :meta,
:monitor,
:name, :name,
:plant_stage, :plant_stage,
:planted_at, :planted_at,
:pointer_type, :pointer_type,
:pullout_direction,
:openfarm_slug,
:radius, :radius,
:tool_id,
:updated_at,
:x, :x,
:y, :y,
:z, :z,
:tool_id,
:gantry_mounted,
:discarded_at,
:monitor,
:created_at,
:updated_at
]) ])
|> validate_required([]) |> validate_required([])
end end

View File

@ -23,7 +23,11 @@ defmodule FarmbotCore.Asset.Supervisor do
end end
def init([]) do def init([]) do
children = [ Supervisor.init(children(), strategy: :one_for_one)
end
def children do
default = [
Repo, Repo,
{AssetSupervisor, module: FbosConfig}, {AssetSupervisor, module: FbosConfig},
{AssetSupervisor, module: FirmwareConfig}, {AssetSupervisor, module: FirmwareConfig},
@ -38,7 +42,7 @@ defmodule FarmbotCore.Asset.Supervisor do
{AssetSupervisor, module: FarmwareEnv}, {AssetSupervisor, module: FarmwareEnv},
AssetMonitor, AssetMonitor,
] ]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Supervisor.init(children, strategy: :one_for_one) Keyword.get(config, :children, default)
end end
end end

View File

@ -82,7 +82,6 @@ defmodule FarmbotCore.AssetMonitor do
sub_state = Map.drop(sub_state, deleted_ids) sub_state = Map.drop(sub_state, deleted_ids)
Enum.each(deleted_ids, fn local_id -> Enum.each(deleted_ids, fn local_id ->
Logger.error("#{inspect(kind)} #{local_id} needs to be terminated")
AssetSupervisor.terminate_child(kind, local_id) AssetSupervisor.terminate_child(kind, local_id)
end) end)
@ -99,7 +98,6 @@ defmodule FarmbotCore.AssetMonitor do
Map.put(sub_state, id, updated_at) Map.put(sub_state, id, updated_at)
compare_datetimes(updated_at, sub_state[id]) == :gt -> compare_datetimes(updated_at, sub_state[id]) == :gt ->
Logger.warn("#{inspect(kind)} #{id} needs to be updated")
asset = Repo.preload(asset, AssetWorker.preload(asset)) asset = Repo.preload(asset, AssetWorker.preload(asset))
:ok = AssetSupervisor.update_child(asset) |> assert_result!(asset) :ok = AssetSupervisor.update_child(asset) |> assert_result!(asset)
Map.put(sub_state, id, updated_at) Map.put(sub_state, id, updated_at)

View File

@ -25,8 +25,6 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
@impl GenServer @impl GenServer
def init([regimen_instance, _args]) do def init([regimen_instance, _args]) do
Logger.warn "RegimenInstance #{inspect(regimen_instance)} initializing"
with %Regimen{} <- regimen_instance.regimen, with %Regimen{} <- regimen_instance.regimen,
%FarmEvent{} <- regimen_instance.farm_event do %FarmEvent{} <- regimen_instance.farm_event do
send self(), :schedule send self(), :schedule
@ -40,25 +38,25 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
def handle_info(:schedule, state) do def handle_info(:schedule, state) do
regimen_instance = state.regimen_instance regimen_instance = state.regimen_instance
# load the sequence and calculate the scheduled_at time # load the sequence and calculate the scheduled_at time
Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) -> Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) ->
scheduled_at = DateTime.add(regimen_instance.epoch, offset, :millisecond) scheduled_at = DateTime.add(regimen_instance.epoch, offset, :millisecond)
sequence = Asset.get_sequence(sequence_id) || raise("sequence #{sequence_id} is not synced") sequence = Asset.get_sequence(sequence_id) || raise("sequence #{sequence_id} is not synced")
%{scheduled_at: scheduled_at, sequence: sequence} %{scheduled_at: scheduled_at, sequence: sequence}
end) end)
# get rid of any item that has already been scheduled/executed # get rid of any item that has already been scheduled/executed
|> Enum.reject(fn(%{scheduled_at: scheduled_at}) -> |> Enum.reject(fn(%{scheduled_at: scheduled_at}) ->
Asset.get_regimen_instance_execution(regimen_instance, scheduled_at) Asset.get_regimen_instance_execution(regimen_instance, scheduled_at)
end) end)
|> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) -> |> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) ->
schedule_sequence(regimen_instance, sequence, at) schedule_sequence(regimen_instance, sequence, at)
end) end)
{:noreply, state} {:noreply, state}
end end
def handle_info({FarmbotCeleryScript, {:scheduled_execution, scheduled_at, executed_at, result}}, state) do def handle_info({FarmbotCeleryScript, {:scheduled_execution, scheduled_at, executed_at, result}}, state) do
status = case result do status = case result do
:ok -> "ok" :ok -> "ok"
{:error, reason} -> {:error, reason} ->
FarmbotCore.Logger.error(2, "Regimen scheduled at #{scheduled_at} failed to execute: #{reason}") FarmbotCore.Logger.error(2, "Regimen scheduled at #{scheduled_at} failed to execute: #{reason}")
reason reason
end end
@ -81,11 +79,11 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
regimen_params = AST.decode(regimen_instance.regimen.body) regimen_params = AST.decode(regimen_instance.regimen.body)
# there may be many sequence scopes from here downward # there may be many sequence scopes from here downward
celery_ast = AST.decode(sequence) celery_ast = AST.decode(sequence)
celery_args = celery_args =
celery_ast.args celery_ast.args
|> Map.put(:sequence_name, sequence.name) |> Map.put(:sequence_name, sequence.name)
|> Map.put(:locals, %{celery_ast.args.locals | body: celery_ast.args.locals.body ++ regimen_params ++ farm_event_params}) |> Map.put(:locals, %{celery_ast.args.locals | body: celery_ast.args.locals.body ++ regimen_params ++ farm_event_params})
celery_ast = %{celery_ast | args: celery_args} celery_ast = %{celery_ast | args: celery_args}
FarmbotCeleryScript.schedule(celery_ast, at, sequence) FarmbotCeleryScript.schedule(celery_ast, at, sequence)
end end

View File

@ -6,11 +6,16 @@ defmodule FarmbotCore.BotState.Supervisor do
end end
def init([]) do def init([]) do
children = [ Supervisor.init(children(), [strategy: :one_for_all])
end
def children do
default = [
FarmbotCore.BotState, FarmbotCore.BotState,
FarmbotCore.BotState.FileSystem, FarmbotCore.BotState.FileSystem,
FarmbotCore.BotState.SchedulerUsageReporter FarmbotCore.BotState.SchedulerUsageReporter
] ]
Supervisor.init(children, [strategy: :one_for_all]) config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
end end
end end

View File

@ -7,9 +7,12 @@ defmodule FarmbotCore.Config.Supervisor do
end end
def init([]) do def init([]) do
children = [ Supervisor.init(children(), strategy: :one_for_one)
{FarmbotCore.Config.Repo, []}, end
]
Supervisor.init(children, strategy: :one_for_one) def children do
default = [ {FarmbotCore.Config.Repo, []} ]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
end end
end end

View File

@ -77,21 +77,21 @@ defmodule FarmbotCore.FirmwareOpenTask do
{:noreply, increment_attempts(%{state | timer: timer})} {:noreply, increment_attempts(%{state | timer: timer})}
firmware_hardware == "none" && needs_open? -> firmware_hardware == "none" && needs_open? ->
FarmbotCore.Logger.debug 3, "Firmware needs to be closed" FarmbotCore.Logger.debug 3, "Closing firmware..."
unswap_transport() unswap_transport()
Config.update_config_value(:bool, "settings", "firmware_needs_open", false) Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
timer = Process.send_after(self(), :open, 5000) timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}} {:noreply, %{state | timer: timer, attempts: 0}}
needs_open? -> needs_open? ->
FarmbotCore.Logger.debug 3, "Firmware needs to be opened" FarmbotCore.Logger.debug 3, "Opening firmware..."
case swap_transport(firmware_path) do case swap_transport(firmware_path) do
:ok -> :ok ->
Config.update_config_value(:bool, "settings", "firmware_needs_open", false) Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
timer = Process.send_after(self(), :open, 5000) timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}} {:noreply, %{state | timer: timer, attempts: 0}}
other -> other ->
FarmbotCore.Logger.debug 3, "Firmware failed to open: #{inspect(other)}" FarmbotCore.Logger.debug 3, "Not ready to open yet, will retry in 5s (#{inspect(other)})"
timer = Process.send_after(self(), :open, 5000) timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}} {:noreply, %{state | timer: timer, attempts: 0}}
end end

View File

@ -1,7 +1,5 @@
defmodule FarmbotCore.Leds do defmodule FarmbotCore.Leds do
@moduledoc "API for controling Farmbot LEDS." @moduledoc "API for controling Farmbot LEDS."
@led_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
@led_handler || Mix.raise("You forgot a led handler!")
@valid_status [:off, :solid, :slow_blink, :fast_blink, :really_fast_blink] @valid_status [:off, :solid, :slow_blink, :fast_blink, :really_fast_blink]
@ -15,29 +13,7 @@ defmodule FarmbotCore.Leds do
def white4(status) when status in @valid_status, do: led_handler().white4(status) def white4(status) when status in @valid_status, do: led_handler().white4(status)
def white5(status) when status in @valid_status, do: led_handler().white5(status) def white5(status) when status in @valid_status, do: led_handler().white5(status)
def factory_test(status) do def led_handler,
red(:off)
blue(:off)
green(:off)
yellow(:off)
white1(:off)
white2(:off)
white3(:off)
white4(:off)
white5(:off)
red(status)
blue(status)
green(status)
yellow(status)
white1(status)
white2(status)
white3(status)
white4(status)
white5(status)
end
defp led_handler,
do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
def child_spec(opts) do def child_spec(opts) do

View File

@ -7,11 +7,13 @@ defmodule FarmbotCore.Logger.Supervisor do
end end
def init([]) do def init([]) do
children = [
supervisor(FarmbotCore.Logger.Repo, [])
]
opts = [strategy: :one_for_all] opts = [strategy: :one_for_all]
supervise(children, opts) supervise(children(), opts)
end
def children do
default = [supervisor(FarmbotCore.Logger.Repo, [])]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
end end
end end

View File

@ -10,11 +10,16 @@ defmodule FarmbotCore.StorageSupervisor do
end end
def init([]) do def init([]) do
children = [ Supervisor.init(children(), [strategy: :one_for_one])
end
def children do
default = [
FarmbotCore.Logger.Supervisor, FarmbotCore.Logger.Supervisor,
FarmbotCore.Config.Supervisor, FarmbotCore.Config.Supervisor,
FarmbotCore.Asset.Supervisor FarmbotCore.Asset.Supervisor
] ]
Supervisor.init(children, [strategy: :one_for_one]) config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
end end
end end

View File

@ -1,21 +1,6 @@
defmodule FarmbotCore.TimeUtils do defmodule FarmbotCore.TimeUtils do
@moduledoc "Helper functions for working with time." @moduledoc "Helper functions for working with time."
def format_time(%DateTime{} = dt) do
"#{format_num(dt.month)}/#{format_num(dt.day)}/#{dt.year} " <>
"at #{format_num(dt.hour)}:#{format_num(dt.minute)}"
end
defp format_num(num), do: :io_lib.format('~2..0B', [num]) |> to_string
# returns midnight of today
@spec build_epoch(DateTime.t) :: DateTime.t
def build_epoch(time) do
tz = FarmbotCore.Asset.fbos_config().timezone
n = Timex.Timezone.convert(time, tz)
Timex.shift(n, hours: -n.hour, seconds: -n.second, minutes: -n.minute)
end
@doc """ @doc """
Compares a datetime with another. Compares a datetime with another.
-1 -- the first date comes before the second one -1 -- the first date comes before the second one

View File

@ -0,0 +1,12 @@
defmodule FarmbotCore.Asset.Repo.Migrations.AddPulloutDirectionToPoint do
use Ecto.Migration
def change do
alter table("points") do
# 0 means "NONE"
add(:pullout_direction, :integer, default: 0)
end
execute("UPDATE points SET updated_at = \"1970-11-07 16:52:31.618000\"")
end
end

View File

@ -14,22 +14,16 @@ defmodule FarmbotCore.Config.Repo.Migrations.AddNtpAndDnsConfigs do
:default_dns_name :default_dns_name
] ]
@config_error """ unless @default_ntp_server_1 && @default_ntp_server_2 && @default_dns_name do
config :farmbot_core, FarmbotCore.EctoMigrator, [ @config_error """
default_ntp_server_1: "0.pool.ntp.org", config :farmbot_core, FarmbotCore.EctoMigrator, [
default_ntp_server_2: "1.pool.ntp.org", default_ntp_server_1: "0.pool.ntp.org",
default_dns_name: "my.farm.bot" default_ntp_server_2: "1.pool.ntp.org",
] default_dns_name: "my.farm.bot"
""" ]
"""
if is_nil(@default_ntp_server_1), Mix.raise(@config_error)
do: raise(@config_error) end
if is_nil(@default_ntp_server_2),
do: raise(@config_error)
if is_nil(@default_dns_name),
do: raise(@config_error)
def change do def change do
create_settings_config( create_settings_config(

View File

@ -27,6 +27,7 @@ defmodule FarmbotCore.Asset.CommandTest do
:ok = Command.update(FirmwareConfig, 23, Map.from_struct(config)) :ok = Command.update(FirmwareConfig, 23, Map.from_struct(config))
end end
@tag :capture_log
test "update / destroy fbos config" do test "update / destroy fbos config" do
params = %{id: 23, update_channel: "whatever"} params = %{id: 23, update_channel: "whatever"}
:ok = Command.update(FbosConfig, 23, params) :ok = Command.update(FbosConfig, 23, params)
@ -38,6 +39,7 @@ defmodule FarmbotCore.Asset.CommandTest do
refute next_config refute next_config
end end
@tag :capture_log
test "update / destroy device" do test "update / destroy device" do
params = %{id: 23, name: "Old Device"} params = %{id: 23, name: "Old Device"}
:ok = Command.update(Device, 23, params) :ok = Command.update(Device, 23, params)
@ -56,6 +58,7 @@ defmodule FarmbotCore.Asset.CommandTest do
assert Asset.get_regimen(id) assert Asset.get_regimen(id)
end end
@tag :capture_log
test "update regimen" do test "update regimen" do
id = id() id = id()
:ok = Command.update("Regimen", id, %{id: id, name: "abc", monitor: false}) :ok = Command.update("Regimen", id, %{id: id, name: "abc", monitor: false})
@ -70,6 +73,7 @@ defmodule FarmbotCore.Asset.CommandTest do
refute Asset.get_regimen(id) refute Asset.get_regimen(id)
end end
@tag :capture_log
test "insert new farm_event" do test "insert new farm_event" do
id = id() id = id()
:ok = Command.update("FarmEvent", id, %{id: id, monitor: false}) :ok = Command.update("FarmEvent", id, %{id: id, monitor: false})
@ -101,6 +105,7 @@ defmodule FarmbotCore.Asset.CommandTest do
assert Asset.get_farm_event(id).executable_type == "Regimen" assert Asset.get_farm_event(id).executable_type == "Regimen"
end end
@tag :capture_log
test "delete farm_event" do test "delete farm_event" do
id = id() id = id()

View File

@ -13,7 +13,7 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
@fake_point_group %PointGroup{ @fake_point_group %PointGroup{
criteria: %{ criteria: %{
"day" => %{"op" => "<", "days_ago" => 4}, "day" => %{"op" => ">", "days_ago" => 4},
"string_eq" => %{ "string_eq" => %{
"openfarm_slug" => ["five", "nine"], "openfarm_slug" => ["five", "nine"],
"meta.created_by" => ["plant-detection"] "meta.created_by" => ["plant-detection"]
@ -24,6 +24,23 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
} }
} }
@simple_point_group %PointGroup{
point_ids: [],
sort_type: "xy_ascending",
criteria: %{
"day" => %{
"op" => "<",
"days_ago" => 0
},
"string_eq" => %{
"pointer_type" => ["Plant"]
},
"number_eq" => %{},
"number_lt" => %{},
"number_gt" => %{}
}
}
# Use this is a fake "Timex.now()" value when mocking. # Use this is a fake "Timex.now()" value when mocking.
@now ~U[2222-12-10 02:22:22.222222Z] @now ~U[2222-12-10 02:22:22.222222Z]
@five_days_ago ~U[2222-12-05 01:11:11.111111Z] @five_days_ago ~U[2222-12-05 01:11:11.111111Z]
@ -90,6 +107,19 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
pg pg
end end
test "direct match on `pointer_type` via `string_eq`" do
Repo.delete_all(PointGroup)
Repo.delete_all(Point)
point!(%{id: 1, pointer_type: "Plant"})
point!(%{id: 2, pointer_type: "Weed"})
point!(%{id: 3, pointer_type: "ToolSlot"})
point!(%{id: 4, pointer_type: "GenericPointer"})
result = CriteriaRetriever.run(@simple_point_group)
assert Enum.count(result) == 1
end
test "run/1" do test "run/1" do
expect(Timex, :now, fn -> @now end) expect(Timex, :now, fn -> @now end)
pg = point_group_with_fake_points() pg = point_group_with_fake_points()
@ -123,6 +153,7 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
Enum.map(expected, fn id -> assert Enum.member?(results, id) end) Enum.map(expected, fn id -> assert Enum.member?(results, id) end)
end end
@tag :capture_log
test "point group that does not define criteria" do test "point group that does not define criteria" do
Repo.delete_all(PointGroup) Repo.delete_all(PointGroup)
Repo.delete_all(Point) Repo.delete_all(Point)
@ -451,7 +482,6 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
"string_eq" => %{} "string_eq" => %{}
}, },
id: 201, id: 201,
local_id: "30856f5e-1f97-4e18-b5e0-84dc7cd9bbf0",
name: "Test (Broke?)", name: "Test (Broke?)",
point_ids: whitelist, point_ids: whitelist,
sort_type: "xy_ascending", sort_type: "xy_ascending",
@ -463,4 +493,70 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
assert Enum.count(whitelist) == Enum.count(results) assert Enum.count(whitelist) == Enum.count(results)
Enum.map(whitelist, fn id -> assert Enum.member?(results, id) end) Enum.map(whitelist, fn id -> assert Enum.member?(results, id) end)
end end
test "edge case: Filter by crop type" do
Repo.delete_all(PointGroup)
Repo.delete_all(Point)
ok = point!(%{id: 1, pointer_type: "Plant", openfarm_slug: "spinach"})
point!(%{id: 2, pointer_type: "Plant", openfarm_slug: "beetroot"})
point!(%{id: 3, pointer_type: "Weed", openfarm_slug: "thistle"})
point!(%{id: 4, pointer_type: "Weed", openfarm_slug: "spinach"})
pg = %PointGroup{
:id => 241,
:point_ids => [],
:criteria => %{
"day" => %{
"op" => "<",
"days_ago" => 0
},
"string_eq" => %{
"pointer_type" => ["Plant"],
"openfarm_slug" => ["spinach"]
},
"number_eq" => %{},
"number_lt" => %{},
"number_gt" => %{}
}
}
ids = CriteriaRetriever.run(pg) |> Enum.map(fn p -> p.id end)
assert Enum.member?(ids, ok.id)
assert Enum.count(ids) == 1
end
test "edge case: Filter by slot direction" do
Repo.delete_all(PointGroup)
Repo.delete_all(Point)
ok = point!(%{id: 1, pointer_type: "ToolSlot", pullout_direction: 3})
point!(%{id: 2, pointer_type: "Weed", pullout_direction: 3})
point!(%{id: 3, pointer_type: "ToolSlot", pullout_direction: 4})
point!(%{id: 4, pointer_type: "GenericPointer", pullout_direction: 2})
pg = %PointGroup{
:id => 242,
:name => "Filter by slot direction",
:point_ids => [],
:sort_type => "xy_ascending",
:criteria => %{
"day" => %{
"op" => "<",
"days_ago" => 0
},
"string_eq" => %{
"pointer_type" => ["ToolSlot"]
},
"number_eq" => %{
"pullout_direction" => [3]
},
"number_lt" => %{},
"number_gt" => %{}
}
}
ids = CriteriaRetriever.run(pg) |> Enum.map(fn p -> p.id end)
assert Enum.member?(ids, ok.id)
assert Enum.count(ids) == 1
end
end end

View File

@ -1,3 +0,0 @@
defmodule FarmbotCore.Asset.PrivateTest do
use ExUnit.Case, async: true
end

View File

@ -1,61 +0,0 @@
defmodule FarmbotCore.AssetMonitorTest do
use ExUnit.Case, async: false
alias FarmbotCore.{Asset.Repo, AssetMonitor, AssetSupervisor}
import Farmbot.TestSupport.AssetFixtures
describe "regimen instances" do
test "adding a regimen instance starts a process" do
farm_event_params = %{
start_time: DateTime.utc_now(),
end_time: DateTime.utc_now(),
repeat: 1,
time_unit: "never"
}
pr = regimen_instance(%{}, farm_event_params, %{monitor: true})
AssetMonitor.force_checkup()
assert {id, _, _, _} = AssetSupervisor.whereis_child(pr)
assert id == pr.local_id
Repo.delete!(pr)
AssetMonitor.force_checkup()
assert {id, :undefined, _, _} = AssetSupervisor.whereis_child(pr)
assert id == pr.local_id
end
end
describe "farm events" do
test "adding a farm event starts a process" do
seq = sequence()
now = DateTime.utc_now()
start_time = Timex.shift(now, minutes: -20)
end_time = Timex.shift(now, minutes: 10)
params = %{
monitor: true,
start_time: start_time,
end_time: end_time,
repeat: 5,
time_unit: "hourly"
}
event = sequence_event(seq, params)
AssetMonitor.force_checkup()
assert {id, _, _, _} = AssetSupervisor.whereis_child(event)
assert id == event.local_id
Repo.delete!(event)
AssetMonitor.force_checkup()
assert {id, :undefined, _, _} = AssetSupervisor.whereis_child(event)
assert id == event.local_id
end
end
end

View File

@ -15,4 +15,10 @@ defmodule FarmbotCore.AssetTest do
assert %RegimenInstance{} = Asset.new_regimen_instance!(event) assert %RegimenInstance{} = Asset.new_regimen_instance!(event)
end end
end end
test "Asset.device/1" do
assert nil == Asset.device(:ota_hour)
assert %FarmbotCore.Asset.Device{} = Asset.update_device!(%{ota_hour: 17})
assert 17 == Asset.device(:ota_hour)
end
end end

View File

@ -4,6 +4,7 @@ defmodule FarmbotCore.FbosConfigWorkerTest do
import Farmbot.TestSupport.AssetFixtures import Farmbot.TestSupport.AssetFixtures
@tag :capture_log
test "adds configs to bot state and config_storage" do test "adds configs to bot state and config_storage" do
%FbosConfig{} = %FbosConfig{} =
conf = conf =

View File

@ -61,6 +61,8 @@ defmodule FarmbotCore.BotState.FileSystemTest do
describe "server" do describe "server" do
test "serializes state to fs" do test "serializes state to fs" do
IO.puts("THIS TEST BLINKS! Fix it.")
root_dir = root_dir =
Path.join([ Path.join([
System.tmp_dir!(), System.tmp_dir!(),
@ -81,7 +83,7 @@ defmodule FarmbotCore.BotState.FileSystemTest do
:ok = BotState.set_pin_value(bot_state_pid, 1, 1) :ok = BotState.set_pin_value(bot_state_pid, 1, 1)
assert_received {BotState, _}, 200 assert_received {BotState, _}, 200
# sleep to allow changes to propagate. # sleep to allow changes to propagate.
Process.sleep(200) Process.sleep(2000)
pins_dir = Path.join([root_dir, "pins", "1"]) pins_dir = Path.join([root_dir, "pins", "1"])
# default value # default value
assert File.read!(Path.join(pins_dir, "mode")) == "-1" assert File.read!(Path.join(pins_dir, "mode")) == "-1"

View File

@ -10,6 +10,7 @@ defmodule FarmbotCore.BotStateTest do
assert_receive {BotState, %Ecto.Changeset{valid?: true}} assert_receive {BotState, %Ecto.Changeset{valid?: true}}
end end
@tag :capture_log
test "invalid data doesn't get dispatched" do test "invalid data doesn't get dispatched" do
{:ok, bot_state_pid} = BotState.start_link([], []) {:ok, bot_state_pid} = BotState.start_link([], [])
_initial_state = BotState.subscribe(bot_state_pid) _initial_state = BotState.subscribe(bot_state_pid)

View File

@ -1,38 +0,0 @@
defmodule FarmbotCore.FarmwareRuntime.PipeWorkerTest do
use ExUnit.Case, async: false
# alias FarmbotCore.FarmwareRuntime.PipeWorker
# TODO Find a suitable tool for testing domain sockets?
# test "reads data from pipe" do
# pipe_name = random_pipe()
# {:ok, pipe_worker} = PipeWorker.start_link(pipe_name)
# ref = PipeWorker.read(pipe_worker, 11)
# {_, 0} = System.cmd("bash", ["-c", "echo -e 'hello world' > #{pipe_name}"])
# assert_receive {PipeWorker, ^ref, {:ok, "hello world"}}
# end
# test "writes data to a pipe" do
# pipe_name = random_pipe()
# {:ok, pipe_worker} = PipeWorker.start_link(pipe_name)
# ref = PipeWorker.read(pipe_worker, 11)
# PipeWorker.write(pipe_worker, "hello world")
# assert_receive {PipeWorker, ^ref, {:ok, "hello world"}}
# end
# test "cleanup pipes on exit" do
# pipe_name = random_pipe()
# {:ok, pipe_worker} = PipeWorker.start_link(pipe_name)
# assert File.exists?(pipe_name)
# _ = Process.flag(:trap_exit, true)
# :ok = PipeWorker.close(pipe_worker)
# assert_receive {:EXIT, ^pipe_worker, :normal}
# refute File.exists?(pipe_name)
# end
# defp random_pipe do
# pipe_name = Ecto.UUID.generate() <> ".pipe"
# Path.join([System.tmp_dir!(), pipe_name])
# end
end

View File

@ -0,0 +1,37 @@
defmodule FarmbotCore.Leds.StubHandlerTest do
use ExUnit.Case, async: true
import ExUnit.CaptureIO
@color_map %{
:red => :red,
:blue => :blue,
:green => :green,
:yellow => :yellow,
:white1 => :white,
:white2 => :white,
:white3 => :white,
:white4 => :white,
:white5 => :white
}
@status [:fast_blink, :really_fast_blink, :slow_blink, :solid]
def capture_led(color) do
status = @status |> Enum.shuffle() |> Enum.at(0)
do_it = fn -> apply(FarmbotCore.Leds, color, [status]) end
cap = capture_io(do_it)
assert cap =~ "LED STATUS:"
assert cap =~ apply(IO.ANSI, Map.fetch!(@color_map, color), [])
end
test "leds" do
capture_led(:red)
capture_led(:blue)
capture_led(:green)
capture_led(:yellow)
capture_led(:white1)
capture_led(:white2)
capture_led(:white3)
capture_led(:white4)
capture_led(:white5)
end
end

View File

@ -0,0 +1,9 @@
defmodule FarmbotCore.LogTest do
alias FarmbotCore.Log
use ExUnit.Case, async: true
test "to_chars" do
log = %Log{message: "Hello, world!"}
assert "Hello, world!" = "#{log}"
end
end

View File

@ -2,6 +2,7 @@ defmodule FarmbotCore.LoggerTest do
use ExUnit.Case use ExUnit.Case
require FarmbotCore.Logger require FarmbotCore.Logger
@tag :capture_log
test "allows handling a log more than once by re-inserting it." do test "allows handling a log more than once by re-inserting it." do
log = FarmbotCore.Logger.debug(1, "Test log ABC") log = FarmbotCore.Logger.debug(1, "Test log ABC")
# Handling a log should delete it from the store. # Handling a log should delete it from the store.

View File

@ -0,0 +1,10 @@
defmodule FarmbotCore.ProjectTest do
use ExUnit.Case
# @opts [cd: Path.join("c_src", "farmbot-arduino-firmware")]
test "arduino_commit" do
actual = FarmbotCore.Project.arduino_commit()
assert is_binary(actual)
assert String.length(actual) == 40
end
end

View File

@ -1,5 +1,16 @@
use Mix.Config use Mix.Config
if Mix.env() == :test do if Mix.env() == :test do
config :farmbot_ext, FarmbotExt, children: [] config :ex_unit, capture_logs: true
mapper = fn mod -> config :farmbot_ext, mod, children: [] end
list = [
FarmbotExt,
FarmbotExt.AMQP.ChannelSupervisor,
FarmbotExt.API.DirtyWorker.Supervisor,
FarmbotExt.API.EagerLoader.Supervisor,
FarmbotExt.Bootstrap.Supervisor
]
Enum.map(list, mapper)
end end

View File

@ -4,13 +4,11 @@ defmodule FarmbotExt do
use Application use Application
def start(_type, _args) do def start(_type, _args) do
opts = [strategy: :one_for_one, name: __MODULE__] Supervisor.start_link(children(), opts())
Supervisor.start_link(children(), opts)
end end
# This only exists because I was getting too many crashed def opts, do: [strategy: :one_for_one, name: __MODULE__]
# supervisor reports in the test suite (distraction from
# real test failures).
def children do def children do
config = Application.get_env(:farmbot_ext, __MODULE__) || [] config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, [FarmbotExt.Bootstrap]) Keyword.get(config, :children, [FarmbotExt.Bootstrap])

View File

@ -19,17 +19,19 @@ defmodule FarmbotExt.AMQP.ChannelSupervisor do
end end
def init([token]) do def init([token]) do
jwt = JWT.decode!(token) Supervisor.init(children(JWT.decode!(token)), strategy: :one_for_one)
end
children = [ def children(jwt) do
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, [
{TelemetryChannel, [jwt: jwt]}, {TelemetryChannel, [jwt: jwt]},
{LogChannel, [jwt: jwt]}, {LogChannel, [jwt: jwt]},
{PingPongChannel, [jwt: jwt]}, {PingPongChannel, [jwt: jwt]},
{BotStateChannel, [jwt: jwt]}, {BotStateChannel, [jwt: jwt]},
{AutoSyncChannel, [jwt: jwt]}, {AutoSyncChannel, [jwt: jwt]},
{CeleryScriptChannel, [jwt: jwt]} {CeleryScriptChannel, [jwt: jwt]}
] ])
Supervisor.init(children, strategy: :one_for_one)
end end
end end

View File

@ -6,7 +6,7 @@ defmodule FarmbotExt.AMQP.PingPongChannel do
Also has a ~15-20 minute timer that will do an `HTTP` request Also has a ~15-20 minute timer that will do an `HTTP` request
to `/api/device`. This refreshed the `last_seen_api` field which to `/api/device`. This refreshed the `last_seen_api` field which
is required for devices that have `auto_sync` enabled as with is required for devices that have `auto_sync` enabled as with
that field enabled, the device would never do an HTTP request that field enabled, the device would never do an HTTP request
""" """
use GenServer use GenServer
@ -99,9 +99,9 @@ defmodule FarmbotExt.AMQP.PingPongChannel do
http_ping_timer = Process.send_after(self(), :http_ping, ms) http_ping_timer = Process.send_after(self(), :http_ping, ms)
{:noreply, %{state | http_ping_timer: http_ping_timer, ping_fails: 0}} {:noreply, %{state | http_ping_timer: http_ping_timer, ping_fails: 0}}
_ -> error ->
ping_fails = state.ping_fails + 1 ping_fails = state.ping_fails + 1
FarmbotCore.Logger.error(3, "Ping failed (#{ping_fails})") FarmbotCore.Logger.error(3, "Ping failed (#{ping_fails}). #{inspect(error)}")
_ = Leds.blue(:off) _ = Leds.blue(:off)
http_ping_timer = Process.send_after(self(), :http_ping, ms) http_ping_timer = Process.send_after(self(), :http_ping, ms)
{:noreply, %{state | http_ping_timer: http_ping_timer, ping_fails: ping_fails}} {:noreply, %{state | http_ping_timer: http_ping_timer, ping_fails: ping_fails}}

View File

@ -10,14 +10,17 @@ defmodule FarmbotExt.AMQP.Supervisor do
end end
def init([]) do def init([]) do
Supervisor.init(children(), strategy: :one_for_all)
end
def children do
token = get_config_value(:string, "authorization", "token") token = get_config_value(:string, "authorization", "token")
email = get_config_value(:string, "authorization", "email") email = get_config_value(:string, "authorization", "email")
config = Application.get_env(:farmbot_ext, __MODULE__) || []
children = [ Keyword.get(config, :children, [
{FarmbotExt.AMQP.ConnectionWorker, [token: token, email: email]}, {FarmbotExt.AMQP.ConnectionWorker, [token: token, email: email]},
{FarmbotExt.AMQP.ChannelSupervisor, [token]} {FarmbotExt.AMQP.ChannelSupervisor, [token]}
] ])
Supervisor.init(children, strategy: :one_for_all)
end end
end end

View File

@ -33,7 +33,13 @@ defmodule FarmbotExt.API.DirtyWorker.Supervisor do
@impl Supervisor @impl Supervisor
def init(_args) do def init(_args) do
children = [ Supervisor.init(children(), strategy: :one_for_one)
end
def children do
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, [
{DirtyWorker, Device}, {DirtyWorker, Device},
{DirtyWorker, DeviceCert}, {DirtyWorker, DeviceCert},
{DirtyWorker, FbosConfig}, {DirtyWorker, FbosConfig},
@ -50,8 +56,6 @@ defmodule FarmbotExt.API.DirtyWorker.Supervisor do
{DirtyWorker, Sensor}, {DirtyWorker, Sensor},
{DirtyWorker, Sequence}, {DirtyWorker, Sequence},
{DirtyWorker, Tool} {DirtyWorker, Tool}
] ])
Supervisor.init(children, strategy: :one_for_one)
end end
end end

View File

@ -39,7 +39,13 @@ defmodule FarmbotExt.API.EagerLoader.Supervisor do
@impl Supervisor @impl Supervisor
def init(_args) do def init(_args) do
children = [ Supervisor.init(children(), strategy: :one_for_one)
end
def children do
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, [
{EagerLoader, Device}, {EagerLoader, Device},
{EagerLoader, FarmEvent}, {EagerLoader, FarmEvent},
{EagerLoader, FarmwareEnv}, {EagerLoader, FarmwareEnv},
@ -56,8 +62,6 @@ defmodule FarmbotExt.API.EagerLoader.Supervisor do
{EagerLoader, Sensor}, {EagerLoader, Sensor},
{EagerLoader, Sequence}, {EagerLoader, Sequence},
{EagerLoader, Tool} {EagerLoader, Tool}
] ])
Supervisor.init(children, strategy: :one_for_one)
end end
end end

View File

@ -16,7 +16,7 @@ defmodule FarmbotExt.API.ImageUploader do
GenServer.start_link(__MODULE__, args, name: __MODULE__) GenServer.start_link(__MODULE__, args, name: __MODULE__)
end end
def force_checkup do def force_checkup() do
GenServer.cast(__MODULE__, :force_checkup) GenServer.cast(__MODULE__, :force_checkup)
end end
@ -45,19 +45,23 @@ defmodule FarmbotExt.API.ImageUploader do
def handle_continue([], state), do: {:noreply, state, @checkup_time_ms} def handle_continue([], state), do: {:noreply, state, @checkup_time_ms}
# the meta here is likely inaccurate here because of # the meta here is likely inaccurate here because of pulling the location data
# pulling the location data from the cache instead of from the firmware # from the cache instead of from the firmware directly. It's close enough and
# directly. It's close enough and getting data from the firmware directly # getting data from the firmware directly would require more work than it is
# would require more work than it is worth # worth
defp try_upload(image_filename) do defp try_upload(image_filename) do
%{x: x, y: y, z: z} = BotState.fetch().location_data.position %{x: x, y: y, z: z} = BotState.fetch().location_data.position
meta = %{x: x, y: y, z: z, name: Path.rootname(image_filename)} meta = %{x: x, y: y, z: z, name: Path.rootname(image_filename)}
finalize(image_filename, API.upload_image(image_filename, meta))
end
with {:ok, %{status: s, body: _body}} when s > 199 and s < 300 <- defp finalize(file, {:ok, %{status: s, body: _}}) when s > 199 and s < 300 do
API.upload_image(image_filename, meta) do FarmbotCore.Logger.success(3, "Uploaded image: #{file}")
FarmbotCore.Logger.success(3, "Uploaded image: #{image_filename}") File.rm(file)
File.rm(image_filename) end
end
defp finalize(fname, other) do
FarmbotCore.Logger.error(3, "Upload Error (#{fname}): #{inspect(other)}")
end end
# Stolen from # Stolen from

View File

@ -12,14 +12,18 @@ defmodule FarmbotExt.Bootstrap.Supervisor do
@impl Supervisor @impl Supervisor
def init([]) do def init([]) do
children = [ Supervisor.init(children(), strategy: :one_for_one)
end
def children() do
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, [
FarmbotExt.API.EagerLoader.Supervisor, FarmbotExt.API.EagerLoader.Supervisor,
FarmbotExt.API.DirtyWorker.Supervisor, FarmbotExt.API.DirtyWorker.Supervisor,
FarmbotExt.AMQP.Supervisor, FarmbotExt.AMQP.Supervisor,
FarmbotExt.API.ImageUploader, FarmbotExt.API.ImageUploader,
FarmbotExt.Bootstrap.DropPasswordTask FarmbotExt.Bootstrap.DropPasswordTask
] ])
Supervisor.init(children, strategy: :one_for_one)
end end
end end

View File

@ -1,5 +1,6 @@
defmodule AutoSyncAssetHandlerTest do defmodule AutoSyncAssetHandlerTest do
use ExUnit.Case, async: true require Helpers
use ExUnit.Case, async: false
use Mimic use Mimic
setup :verify_on_exit! setup :verify_on_exit!
@ -8,7 +9,10 @@ defmodule AutoSyncAssetHandlerTest do
alias FarmbotExt.AMQP.AutoSyncAssetHandler alias FarmbotExt.AMQP.AutoSyncAssetHandler
alias FarmbotCore.{Asset, BotState, Leds} alias FarmbotCore.{Asset, BotState, Leds}
import ExUnit.CaptureLog
def auto_sync_off, do: expect(Asset.Query, :auto_sync?, fn -> false end) def auto_sync_off, do: expect(Asset.Query, :auto_sync?, fn -> false end)
def auto_sync_on, do: expect(Asset.Query, :auto_sync?, fn -> true end)
def expect_sync_status_to_be(status), def expect_sync_status_to_be(status),
do: expect(BotState, :set_sync_status, fn ^status -> :ok end) do: expect(BotState, :set_sync_status, fn ^status -> :ok end)
@ -22,4 +26,41 @@ defmodule AutoSyncAssetHandlerTest do
expect_green_leds(:slow_blink) expect_green_leds(:slow_blink)
AutoSyncAssetHandler.handle_asset("Point", 23, nil) AutoSyncAssetHandler.handle_asset("Point", 23, nil)
end end
test "Handles @no_cache_kinds" do
id = 64
params = %{}
kind =
~w(Device FbosConfig FirmwareConfig FarmwareEnv FarmwareInstallation)
|> Enum.shuffle()
|> Enum.at(0)
expect(Asset.Command, :update, 1, fn ^kind, ^id, ^params -> :ok end)
assert :ok = AutoSyncAssetHandler.cache_sync(kind, id, params)
end
test "handling of deleted assets when auto_sync is enabled" do
auto_sync_on()
expect_sync_status_to_be("syncing")
expect_sync_status_to_be("synced")
expect_green_leds(:really_fast_blink)
expect_green_leds(:solid)
AutoSyncAssetHandler.handle_asset("Point", 32, nil)
end
test "cache sync" do
id = 64
params = %{}
kind = "Point"
# Helpers.expect_log("Autocaching sync #{kind} #{id} #{inspect(params)}")
changeset = %{ab: :cd}
changesetfaker = fn ^kind, ^id, ^params -> changeset end
expect(FarmbotCore.Asset.Command, :new_changeset, 1, changesetfaker)
expect(FarmbotExt.API.EagerLoader, :cache, 1, fn ^changeset -> :ok end)
expect_sync_status_to_be("sync_now")
expect_green_leds(:slow_blink)
do_it = fn -> AutoSyncAssetHandler.cache_sync(kind, id, params) end
assert capture_log(do_it) =~ "Autocaching sync Point 64 %{}"
end
end end

View File

@ -1,6 +1,6 @@
defmodule AutoSyncChannelTest do defmodule AutoSyncChannelTest do
require Helpers require Helpers
use ExUnit.Case, async: true use ExUnit.Case, async: false
use Mimic use Mimic
alias FarmbotExt.AMQP.AutoSyncChannel alias FarmbotExt.AMQP.AutoSyncChannel
@ -55,7 +55,7 @@ defmodule AutoSyncChannelTest do
expect(Preloader, :preload_all, 1, fn -> :ok end) expect(Preloader, :preload_all, 1, fn -> :ok end)
pid = generate_pid() pid = generate_pid()
send(pid, msg) send(pid, msg)
Process.sleep(5) Helpers.wait_for(pid)
end end
test "basic_cancel", do: ensure_response_to({:basic_cancel, :anything}) test "basic_cancel", do: ensure_response_to({:basic_cancel, :anything})
@ -78,6 +78,7 @@ defmodule AutoSyncChannelTest do
# Helpers.expect_log("Failed to connect to AutoSync channel: :whatever") # Helpers.expect_log("Failed to connect to AutoSync channel: :whatever")
# Helpers.expect_log("Disconnected from AutoSync channel: :normal") # Helpers.expect_log("Disconnected from AutoSync channel: :normal")
pid = generate_pid() pid = generate_pid()
IO.puts(" =====RICK: This test blinks and you should fix it.")
assert %{chan: nil, conn: nil, preloaded: true} == AutoSyncChannel.network_status(pid) assert %{chan: nil, conn: nil, preloaded: true} == AutoSyncChannel.network_status(pid)
GenServer.stop(pid, :normal) GenServer.stop(pid, :normal)
end end
@ -153,6 +154,7 @@ defmodule AutoSyncChannelTest do
:ok :ok
end) end)
Helpers.wait_for(pid)
Process.sleep(1000) Process.sleep(1000)
end end
end end

View File

@ -1,5 +1,5 @@
defmodule FarmbotExt.AMQP.BotStateChannelTest do defmodule FarmbotExt.AMQP.BotStateChannelTest do
use ExUnit.Case use ExUnit.Case, async: false
use Mimic use Mimic
# alias FarmbotExt.AMQP.BotStateChannel # alias FarmbotExt.AMQP.BotStateChannel

View File

@ -0,0 +1,43 @@
defmodule FarmbotExt.API.ImageUploaderTest do
require Helpers
use ExUnit.Case, async: false
use Mimic
alias FarmbotExt.API.ImageUploader
setup :verify_on_exit!
setup :set_mimic_global
test "force checkup" do
pid =
if Process.whereis(ImageUploader) do
Process.whereis(ImageUploader)
else
{:ok, p} = ImageUploader.start_link([])
p
end
["a.jpg", "b.jpeg", "c.png", "d.gif"]
|> Enum.map(fn fname ->
f = "/tmp/images/#{fname}"
File.touch!(f)
File.write(f, "X")
end)
expect(FarmbotExt.API, :upload_image, 4, fn
"/tmp/images/d.gif", _meta -> {:error, %{status: 401, body: %{}}}
_image_filename, _meta -> {:ok, %{status: 201, body: %{}}}
end)
err_msg =
"Upload Error (/tmp/images/d.gif): " <>
"{:error, %{body: %{}, status: 401}}"
Helpers.expect_log("Uploaded image: /tmp/images/a.jpg")
Helpers.expect_log("Uploaded image: /tmp/images/b.jpeg")
Helpers.expect_log("Uploaded image: /tmp/images/c.png")
Helpers.expect_log(err_msg)
ImageUploader.force_checkup()
send(pid, :timeout)
Helpers.wait_for(pid)
end
end

View File

@ -1,28 +1,54 @@
Application.ensure_all_started(:farmbot) Application.ensure_all_started(:farmbot)
Mimic.copy(AMQP.Channel) [
Mimic.copy(FarmbotCeleryScript.SysCalls.Stubs) AMQP.Channel,
Mimic.copy(FarmbotCore.Asset.Command) FarmbotCeleryScript.SysCalls.Stubs,
Mimic.copy(FarmbotCore.Asset.Query) FarmbotCore.Asset.Command,
Mimic.copy(FarmbotCore.BotState) FarmbotCore.Asset.Query,
Mimic.copy(FarmbotCore.Leds) FarmbotCore.BotState,
Mimic.copy(FarmbotCore.LogExecutor) FarmbotCore.Leds,
Mimic.copy(FarmbotExt.AMQP.ConnectionWorker) FarmbotCore.LogExecutor,
Mimic.copy(FarmbotExt.API.EagerLoader.Supervisor) FarmbotExt.AMQP.AutoSyncAssetHandler,
Mimic.copy(FarmbotExt.API.Preloader) FarmbotExt.AMQP.ConnectionWorker,
Mimic.copy(FarmbotExt.API) FarmbotExt.API,
Mimic.copy(FarmbotExt.AMQP.AutoSyncAssetHandler) FarmbotExt.API.EagerLoader,
FarmbotExt.API.EagerLoader.Supervisor,
FarmbotExt.API.Preloader
]
|> Enum.map(&Mimic.copy/1)
timeout = System.get_env("EXUNIT_TIMEOUT") timeout = System.get_env("EXUNIT_TIMEOUT") || "5000"
System.put_env("LOG_SILENCE", "true") System.put_env("LOG_SILENCE", "true")
if timeout do ExUnit.start(assert_receive_timeout: String.to_integer(timeout))
ExUnit.start(assert_receive_timeout: String.to_integer(timeout))
else
ExUnit.start()
end
defmodule Helpers do defmodule Helpers do
# Maybe I don't need this?
# Maybe I could use `start_supervised`?
# https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#start_supervised/2
@wait_time 180
# Base case: We have a pid
def wait_for(pid) when is_pid(pid), do: check_on_mbox(pid)
# Failure case: We failed to find a pid for a module.
def wait_for(nil), do: raise("Attempted to wait on bad module/pid")
# Edge case: We have a module and need to try finding its pid.
def wait_for(mod), do: wait_for(Process.whereis(mod))
# Enter recursive loop
defp check_on_mbox(pid) do
Process.sleep(@wait_time)
wait(pid, Process.info(pid, :message_queue_len))
end
# Exit recursive loop (mbox is clear)
defp wait(_, {:message_queue_len, 0}), do: Process.sleep(@wait_time * 3)
# Exit recursive loop (pid is dead)
defp wait(_, nil), do: Process.sleep(@wait_time * 3)
# Continue recursive loop
defp wait(pid, {:message_queue_len, _n}), do: check_on_mbox(pid)
defmacro expect_log(message) do defmacro expect_log(message) do
quote do quote do
expect(FarmbotCore.LogExecutor, :execute, fn log -> expect(FarmbotCore.LogExecutor, :execute, fn log ->

View File

@ -78,28 +78,6 @@ defmodule FarmbotFirmware do
and reply with `:ok | {:error, term()}` and reply with `:ok | {:error, term()}`
# VCR
This server can save all the input and output gcodes to a text file for
further external analysis or playback later.
## Using VCR mode
The server can be started in VCR mode by doing:
FarmbotFirmware.start_link([transport: FarmbotFirmware.StubTransport, vcr_path: "/tmp/vcr.txt"], [])
or can be started at runtime:
FarmbotFirmware.enter_vcr_mode(firmware_server, "/tmp/vcr.txt")
in either case the VCR recording needs to be stopped:
FarmbotFirmware.exit_vcr_mode(firmware_server)
VCRs can later be played back:
FarmbotFirmware.VCR.playback!("/tmp/vcr.txt")
""" """
use GenServer use GenServer
require Logger require Logger
@ -129,7 +107,6 @@ defmodule FarmbotFirmware do
:command_queue, :command_queue,
:caller_pid, :caller_pid,
:current, :current,
:vcr_fd,
:reset, :reset,
:reset_pid :reset_pid
] ]
@ -146,7 +123,6 @@ defmodule FarmbotFirmware do
command_queue: [{pid(), GCODE.t()}], command_queue: [{pid(), GCODE.t()}],
caller_pid: nil | pid, caller_pid: nil | pid,
current: nil | GCODE.t(), current: nil | GCODE.t(),
vcr_fd: nil | File.io_device(),
reset: module(), reset: module(),
reset_pid: nil | pid() reset_pid: nil | pid()
} }
@ -209,22 +185,6 @@ defmodule FarmbotFirmware do
GenServer.call(server, :reset) GenServer.call(server, :reset)
end end
@doc """
Sets the Firmware server to record input and output GCODES
to a pair of text files.
"""
def enter_vcr_mode(server \\ __MODULE__, tape_path) do
GenServer.call(server, {:enter_vcr_mode, tape_path})
end
@doc """
Sets the Firmware server to stop recording input and output
GCODES.
"""
def exit_vcr_mode(server \\ __MODULE__) do
GenServer.cast(server, :exit_vcr_mode)
end
@doc """ @doc """
Starting the Firmware server requires at least: Starting the Firmware server requires at least:
* `:transport` - a module implementing the Transport GenServer behaviour. * `:transport` - a module implementing the Transport GenServer behaviour.
@ -252,18 +212,6 @@ defmodule FarmbotFirmware do
# probably? # probably?
reset = Keyword.get(args, :reset) || FarmbotFirmware.NullReset reset = Keyword.get(args, :reset) || FarmbotFirmware.NullReset
vcr_fd =
case Keyword.get(args, :vcr_path) do
nil ->
nil
tape_path ->
{:ok, vcr_fd} =
File.open(tape_path, [:binary, :append, :exclusive, :write])
vcr_fd
end
# Add an anon function that transport implementations should call. # Add an anon function that transport implementations should call.
fw = self() fw = self()
fun = fn {_, _} = code -> GenServer.cast(fw, code) end fun = fn {_, _} = code -> GenServer.cast(fw, code) end
@ -279,11 +227,10 @@ defmodule FarmbotFirmware do
reset: reset, reset: reset,
reset_pid: nil, reset_pid: nil,
command_queue: [], command_queue: [],
configuration_queue: [], configuration_queue: []
vcr_fd: vcr_fd
} }
send(self(), :timeout) send_timeout_self()
{:ok, state} {:ok, state}
end end
@ -347,7 +294,7 @@ defmodule FarmbotFirmware do
] ]
} = state } = state
) do ) do
case GenServer.call(state.transport_pid, {tag, code}) do case call_transport(state.transport_pid, {tag, code}, 297) do
:ok -> :ok ->
new_state = %{ new_state = %{
state state
@ -358,7 +305,6 @@ defmodule FarmbotFirmware do
} }
_ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}]) _ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
_ = vcr_write(state, :out, {state.tag, code})
{:noreply, new_state} {:noreply, new_state}
@ -370,11 +316,10 @@ defmodule FarmbotFirmware do
def handle_info(:timeout, %{configuration_queue: [code | rest]} = state) do def handle_info(:timeout, %{configuration_queue: [code | rest]} = state) do
# Logger.debug("Starting next configuration code: #{inspect(code)}") # Logger.debug("Starting next configuration code: #{inspect(code)}")
case GenServer.call(state.transport_pid, {state.tag, code}) do case call_transport(state.transport_pid, {state.tag, code}, 319) do
:ok -> :ok ->
new_state = %{state | current: code, configuration_queue: rest} new_state = %{state | current: code, configuration_queue: rest}
_ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}]) _ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
_ = vcr_write(state, :out, {state.tag, code})
{:noreply, new_state} {:noreply, new_state}
error -> error ->
@ -389,7 +334,6 @@ defmodule FarmbotFirmware do
for {pid, _code} <- state.command_queue, for {pid, _code} <- state.command_queue,
do: send(pid, {state.tag, {:report_busy, []}}) do: send(pid, {state.tag, {:report_busy, []}})
# Logger.debug "Got checkup message when current command still executing"
{:noreply, state} {:noreply, state}
end end
@ -400,7 +344,7 @@ defmodule FarmbotFirmware do
for {pid, _code} <- state.command_queue, for {pid, _code} <- state.command_queue,
do: send(pid, {state.tag, {:report_busy, []}}) do: send(pid, {state.tag, {:report_busy, []}})
case GenServer.call(state.transport_pid, {tag, code}) do case call_transport(state.transport_pid, {tag, code}, 348) do
:ok -> :ok ->
new_state = %{ new_state = %{
state state
@ -411,7 +355,6 @@ defmodule FarmbotFirmware do
} }
_ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}]) _ = side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
_ = vcr_write(state, :out, {state.tag, code})
for {pid, _code} <- rest, do: send(pid, {state.tag, {:report_busy, []}}) for {pid, _code} <- rest, do: send(pid, {state.tag, {:report_busy, []}})
{:noreply, new_state} {:noreply, new_state}
@ -438,7 +381,12 @@ defmodule FarmbotFirmware do
true = Process.demonitor(state.transport_ref) true = Process.demonitor(state.transport_ref)
end end
:ok = GenServer.stop(state.transport_pid, :normal) if is_pid(state.transport_pid) do
Logger.debug("closing transport")
:ok = GenServer.stop(state.transport_pid, :normal)
else
Logger.debug("No tranport pid found. Nothing to close")
end
next_state = next_state =
goto( goto(
@ -473,7 +421,7 @@ defmodule FarmbotFirmware do
next_state = %{state | transport: module, transport_args: transport_args} next_state = %{state | transport: module, transport_args: transport_args}
send(self(), :timeout) send_timeout_self()
{:reply, :ok, next_state} {:reply, :ok, next_state}
end end
@ -485,16 +433,6 @@ defmodule FarmbotFirmware do
{:reply, {:error, s}, state} {:reply, {:error, s}, state}
end end
def handle_call({:enter_vcr_mode, tape_path}, _from, state) do
with {:ok, vcr_fd} <-
File.open(tape_path, [:binary, :append, :exclusive, :write]) do
{:reply, :ok, %{state | vcr_fd: vcr_fd}}
else
error ->
{:reply, error, state}
end
end
def handle_call({tag, {kind, args}}, from, state) do def handle_call({tag, {kind, args}}, from, state) do
handle_command({tag, {kind, args}}, from, state) handle_command({tag, {kind, args}}, from, state)
end end
@ -521,7 +459,7 @@ defmodule FarmbotFirmware do
for {pid, _code} <- state.command_queue, for {pid, _code} <- state.command_queue,
do: send(pid, {state.tag, {:report_emergency_lock, []}}) do: send(pid, {state.tag, {:report_emergency_lock, []}})
send(self(), :timeout) send_timeout_self()
{:reply, {:ok, tag}, {:reply, {:ok, tag},
%{state | command_queue: [{pid, code}], configuration_queue: []}} %{state | command_queue: [{pid, code}], configuration_queue: []}}
@ -533,7 +471,7 @@ defmodule FarmbotFirmware do
{pid, _ref}, {pid, _ref},
state state
) do ) do
send(self(), :timeout) send_timeout_self()
{:reply, {:ok, tag}, {:reply, {:ok, tag},
%{state | command_queue: [{pid, code}], configuration_queue: []}} %{state | command_queue: [{pid, code}], configuration_queue: []}}
@ -541,7 +479,7 @@ defmodule FarmbotFirmware do
# If not in an acceptable state, return an error immediately. # If not in an acceptable state, return an error immediately.
def handle_command(_, _, %{status: s} = state) def handle_command(_, _, %{status: s} = state)
when s in [:transport_boot, :boot, :no_config, :configuration] do when s in [:boot, :no_config] do
{:reply, {:error, "Can't send command when in #{inspect(s)} state"}, state} {:reply, {:error, "Can't send command when in #{inspect(s)} state"}, state}
end end
@ -550,14 +488,14 @@ defmodule FarmbotFirmware do
case {new_state.status, state.current} do case {new_state.status, state.current} do
{:idle, nil} -> {:idle, nil} ->
send(self(), :timeout) send_timeout_self()
{:reply, {:ok, tag}, new_state} {:reply, {:ok, tag}, new_state}
# Don't do any flow control if state is emergency_lock. # Don't do any flow control if state is emergency_lock.
# This allows a transport to decide # This allows a transport to decide
# if a command should be blocked or not. # if a command should be blocked or not.
{:emergency_lock, _} -> {:emergency_lock, _} ->
send(self(), :timeout) send_timeout_self()
{:reply, {:ok, tag}, new_state} {:reply, {:ok, tag}, new_state}
_unknown -> _unknown ->
@ -565,15 +503,9 @@ defmodule FarmbotFirmware do
end end
end end
def handle_cast(:exit_vcr_mode, state) do
state.vcr_fd && File.close(state.vcr_fd)
{:noreply, %{state | vcr_fd: nil}}
end
# Extracts tag # Extracts tag
def handle_cast({tag, {_, _} = code}, state) do def handle_cast({tag, {_, _} = code}, state) do
_ = side_effects(state, :handle_input_gcode, [{tag, code}]) _ = side_effects(state, :handle_input_gcode, [{tag, code}])
_ = vcr_write(state, :in, {tag, code})
handle_report(code, %{state | tag: tag}) handle_report(code, %{state | tag: tag})
end end
@ -585,7 +517,7 @@ defmodule FarmbotFirmware do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code}) if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
for {pid, _code} <- state.command_queue, do: send(pid, code) for {pid, _code} <- state.command_queue, do: send(pid, code)
send(self(), :timeout) send_timeout_self()
{:noreply, goto(%{state | current: nil, caller_pid: nil}, :emergency_lock)} {:noreply, goto(%{state | current: nil, caller_pid: nil}, :emergency_lock)}
end end
@ -598,10 +530,7 @@ defmodule FarmbotFirmware do
handle_report({:report_no_config, []}, state) handle_report({:report_no_config, []}, state)
end end
def handle_report( def handle_report({:report_idle, []}, %{status: :boot} = state) do
{:report_idle, []},
%{status: :boot} = state
) do
Logger.info("ARDUINO STARTUP COMPLETE (idle) transport=#{state.transport}") Logger.info("ARDUINO STARTUP COMPLETE (idle) transport=#{state.transport}")
handle_report({:report_no_config, []}, state) handle_report({:report_no_config, []}, state)
end end
@ -648,7 +577,7 @@ defmodule FarmbotFirmware do
do: to_process ++ [{:command_movement_find_home, [:x]}], do: to_process ++ [{:command_movement_find_home, [:x]}],
else: to_process else: to_process
send(self(), :timeout) send_timeout_self()
{:noreply, {:noreply,
goto(%{state | tag: tag, configuration_queue: to_process}, :configuration)} goto(%{state | tag: tag, configuration_queue: to_process}, :configuration)}
@ -684,7 +613,7 @@ defmodule FarmbotFirmware do
side_effects(state, :handle_busy, [false]) side_effects(state, :handle_busy, [false])
side_effects(state, :handle_idle, [true]) side_effects(state, :handle_idle, [true])
send(self(), :timeout) send_timeout_self()
{:noreply, goto(%{state | caller_pid: nil, current: nil}, :idle)} {:noreply, goto(%{state | caller_pid: nil, current: nil}, :idle)}
end end
@ -705,7 +634,7 @@ defmodule FarmbotFirmware do
new_state = %{state | current: nil, caller_pid: nil} new_state = %{state | current: nil, caller_pid: nil}
side_effects(state, :handle_busy, [false]) side_effects(state, :handle_busy, [false])
send(self(), :timeout) send_timeout_self()
{:noreply, goto(new_state, :idle)} {:noreply, goto(new_state, :idle)}
end end
@ -739,7 +668,7 @@ defmodule FarmbotFirmware do
do: send(pid, {state.tag, {:report_busy, []}}) do: send(pid, {state.tag, {:report_busy, []}})
side_effects(state, :handle_busy, [false]) side_effects(state, :handle_busy, [false])
send(self(), :timeout) send_timeout_self()
{:noreply, %{state | caller_pid: nil, current: nil}} {:noreply, %{state | caller_pid: nil, current: nil}}
end end
@ -761,7 +690,7 @@ defmodule FarmbotFirmware do
for {pid, _code} <- state.command_queue, for {pid, _code} <- state.command_queue,
do: send(pid, {state.tag, {:report_busy, []}}) do: send(pid, {state.tag, {:report_busy, []}})
send(self(), :timeout) send_timeout_self()
{:noreply, %{state | caller_pid: nil, current: nil}} {:noreply, %{state | caller_pid: nil, current: nil}}
end end
@ -796,7 +725,7 @@ defmodule FarmbotFirmware do
to_process = [{:parameter_write, param}] to_process = [{:parameter_write, param}]
side_effects(state, :handle_parameter_value, [param]) side_effects(state, :handle_parameter_value, [param])
side_effects(state, :handle_parameter_calibration_value, [param]) side_effects(state, :handle_parameter_calibration_value, [param])
send(self(), :timeout) send_timeout_self()
{:noreply, {:noreply,
goto( goto(
@ -996,30 +925,28 @@ defmodule FarmbotFirmware do
defp side_effects(%{side_effects: m}, function, args), defp side_effects(%{side_effects: m}, function, args),
do: apply(m, function, args) do: apply(m, function, args)
@spec vcr_write(state, :in | :out, GCODE.t()) :: :ok defp send_timeout_self do
defp vcr_write(%{vcr_fd: nil}, _direction, _code), do: :ok send(self(), :timeout)
end
defp vcr_write(state, :in, code), do: vcr_write(state, "<", code) defp call_transport(nil, args, where) do
msg =
"#{inspect(where)} Firmware not ready. A restart may be required if not already started (#{
inspect(args)
})"
defp vcr_write(state, :out, code), do: vcr_write(state, "\n>", code) Logger.debug(msg)
{:error, msg}
end
defp vcr_write(state, direction, code) do defp call_transport(transport_pid, args, where) do
data = GCODE.encode(code) # Returns :ok
time = :os.system_time(:second) response = GenServer.call(transport_pid, args)
current_data = unless response == :ok do
if state.current do Logger.debug("#{inspect(where)}: returned #{inspect(response)}")
GCODE.encode({state.tag, state.current}) end
else
"nil"
end
state_data = response
"#{state.status} | #{current_data} | #{inspect(state.caller_pid)}"
IO.write(
state.vcr_fd,
direction <> " #{time} " <> data <> " state=" <> state_data <> "\n"
)
end end
end end

View File

@ -6,6 +6,7 @@ defmodule FarmbotFirmware.CommandTest do
import ExUnit.CaptureLog import ExUnit.CaptureLog
@subject FarmbotFirmware.Command @subject FarmbotFirmware.Command
@tag :capture_log
test "command() runs RPCs" do test "command() runs RPCs" do
arg = [transport: FarmbotFirmware.StubTransport] arg = [transport: FarmbotFirmware.StubTransport]
{:ok, pid} = FarmbotFirmware.start_link(arg, []) {:ok, pid} = FarmbotFirmware.start_link(arg, [])
@ -19,6 +20,7 @@ defmodule FarmbotFirmware.CommandTest do
assert :ok == FarmbotFirmware.command(pid, cmd) assert :ok == FarmbotFirmware.command(pid, cmd)
end end
@tag :capture_log
test "command() refuses to run RPCs in :boot state" do test "command() refuses to run RPCs in :boot state" do
arg = [transport: FarmbotFirmware.StubTransport] arg = [transport: FarmbotFirmware.StubTransport]
{:ok, pid} = FarmbotFirmware.start_link(arg, []) {:ok, pid} = FarmbotFirmware.start_link(arg, [])

View File

@ -4,6 +4,7 @@ defmodule FarmbotFirmware.UARTTransportTest do
doctest FarmbotFirmware.UARTTransport doctest FarmbotFirmware.UARTTransport
alias FarmbotFirmware.{UartDefaultAdapter, UARTTransport} alias FarmbotFirmware.{UartDefaultAdapter, UARTTransport}
setup :verify_on_exit! setup :verify_on_exit!
import ExUnit.CaptureLog
test "UARTTransport.init/1" do test "UARTTransport.init/1" do
expect(UartDefaultAdapter, :start_link, fn -> expect(UartDefaultAdapter, :start_link, fn ->
@ -61,15 +62,22 @@ defmodule FarmbotFirmware.UARTTransportTest do
fake_opts fake_opts
end) end)
error = "Simulated UART failure. This is OK"
expect(UartDefaultAdapter, :open, fn _, _, _ -> expect(UartDefaultAdapter, :open, fn _, _, _ ->
{:error, "Simulated UART failure. This is OK"} {:error, error}
end) end)
{:noreply, state2, retry_timeout} = logs =
UARTTransport.handle_info(:timeout, state) capture_log(fn ->
{:noreply, state2, retry_timeout} =
UARTTransport.handle_info(:timeout, state)
assert retry_timeout == 5000 assert retry_timeout == 5000
assert state.open == state2.open assert state.open == state2.open
end)
assert logs =~ error
end end
test "UARTTransport handles `Circuits-UART` speecific errors" do test "UARTTransport handles `Circuits-UART` speecific errors" do

View File

@ -19,6 +19,7 @@ defmodule FarmbotFirmwareTest do
pid pid
end end
@tag :capture_log
test "various reports" do test "various reports" do
pid = firmware_server() pid = firmware_server()
@ -63,6 +64,7 @@ defmodule FarmbotFirmwareTest do
Process.sleep(1000) Process.sleep(1000)
end end
@tag :capture_log
test "various command()s" do test "various command()s" do
pid = firmware_server() pid = firmware_server()

View File

@ -49,6 +49,9 @@ defmodule FarmbotFirmware.PackageUtilsTest do
{:ok, path} = PackageUtils.find_hex_file("express_k10") {:ok, path} = PackageUtils.find_hex_file("express_k10")
assert String.contains?(path, "/farmbot_firmware/priv/express_k10.hex") assert String.contains?(path, "/farmbot_firmware/priv/express_k10.hex")
{:ok, path} = PackageUtils.find_hex_file("none")
assert path =~ "lib/farmbot_firmware/priv/eeprom_clear.ino.hex"
assert {:error, "unknown firmware hardware: no"} == assert {:error, "unknown firmware hardware: no"} ==
PackageUtils.find_hex_file("no") PackageUtils.find_hex_file("no")
end end

View File

@ -1,6 +1,11 @@
defmodule FarmbotFirmware.ParamTest do defmodule FarmbotFirmware.ParamTest do
use ExUnit.Case use ExUnit.Case
alias FarmbotFirmware.Param alias FarmbotFirmware.Param
import ExUnit.CaptureLog
def t(p, v, expected) do
assert Param.to_human(p, v) == expected
end
test "to_human()" do test "to_human()" do
float_value = 1.23 float_value = 1.23
@ -9,212 +14,354 @@ defmodule FarmbotFirmware.ParamTest do
steps_per_s = "(steps/s)" steps_per_s = "(steps/s)"
steps_per_mm = "(steps/mm)" steps_per_mm = "(steps/mm)"
assert Param.to_human(:param_test, 1) == t(:pin_guard_5_time_out, 12, {"pin guard 5 timeout", "(seconds)", "12"})
{"param_test", nil, true} t(:pin_guard_5_pin_nr, 12, {"pin guard 5 pin number", nil, "12"})
t(:pin_guard_5_active_state, 0, {"pin guard 5 safe state", nil, "HIGH"})
assert Param.to_human(:param_config_ok, 1) == t(:pin_guard_4_time_out, 12, {"pin guard 4 timeout", "(seconds)", "12"})
{"param_config_ok", nil, true} t(:pin_guard_4_pin_nr, 12, {"pin guard 4 pin number", nil, "12"})
t(:pin_guard_4_active_state, 0, {"pin guard 4 safe state", nil, "HIGH"})
assert Param.to_human(:param_use_eeprom, 1) == t(:pin_guard_3_time_out, 1.0, {"pin guard 3 timeout", "(seconds)", "1"})
{"use eeprom", nil, true} t(:pin_guard_3_pin_nr, 1.0, {"pin guard 3 pin number", nil, "1"})
t(:pin_guard_3_active_state, 0, {"pin guard 3 safe state", nil, "HIGH"})
assert Param.to_human(:param_e_stop_on_mov_err, 1) == t(:pin_guard_2_time_out, 1.0, {"pin guard 2 timeout", "(seconds)", "1"})
{"e-stop on movement errors", nil, true} t(:pin_guard_2_pin_nr, 1.0, {"pin guard 2 pin number", nil, "1"})
t(:pin_guard_2_active_state, 0, {"pin guard 2 safe state", nil, "HIGH"})
assert Param.to_human(:movement_timeout_x, float_value) == t(:pin_guard_1_time_out, 1.0, {"pin guard 1 timeout", "(seconds)", "1"})
{"timeout after, x-axis", seconds, "1.2"} t(:pin_guard_1_pin_nr, 1.0, {"pin guard 1 pin number", nil, "1"})
t(:pin_guard_1_active_state, 0, {"pin guard 1 safe state", nil, "HIGH"})
assert Param.to_human(:movement_timeout_y, float_value) == t(:param_use_eeprom, 1, {"use eeprom", nil, true})
{"timeout after, y-axis", seconds, "1.2"} t(:param_test, 1, {"param_test", nil, true})
t(:param_mov_nr_retry, 1.0, {"max retries", nil, "1"})
assert Param.to_human(:movement_timeout_z, float_value) == t(:param_e_stop_on_mov_err, 1, {"e-stop on movement errors", nil, true})
{"timeout after, z-axis", seconds, "1.2"} t(:param_config_ok, 1, {"param_config_ok", nil, true})
t(:movement_stop_at_max_z, 1, {"stop at max, z-axis", nil, true})
assert Param.to_human(:movement_keep_active_x, 1) == t(:movement_stop_at_max_y, 1, {"stop at max, y-axis", nil, true})
{"always power motors, x-axis", nil, true} t(:movement_stop_at_max_x, 1, {"stop at max, x-axis", nil, true})
t(:movement_stop_at_home_z, 1, {"stop at home, z-axis", nil, true})
assert Param.to_human(:movement_keep_active_y, 1) == t(:movement_stop_at_home_y, 1, {"stop at home, y-axis", nil, true})
{"always power motors, y-axis", nil, true} t(:movement_stop_at_home_x, 1, {"stop at home, x-axis", nil, true})
t(:movement_secondary_motor_x, 1, {"enable 2nd x motor", nil, true})
assert Param.to_human(:movement_keep_active_z, 1) == t(:movement_secondary_motor_invert_x, 1, {"invert 2nd x motor", nil, true})
{"always power motors, z-axis", nil, true} t(:movement_microsteps_z, float_value, {"microsteps, z-axis", nil, "1.2"})
t(:movement_microsteps_y, float_value, {"microsteps, y-axis", nil, "1.2"})
assert Param.to_human(:movement_home_at_boot_x, 1) == t(:movement_microsteps_x, float_value, {"microsteps, x-axis", nil, "1.2"})
{"find home on boot, x-axis", nil, true} t(:movement_keep_active_z, 1, {"always power motors, z-axis", nil, true})
t(:movement_keep_active_y, 1, {"always power motors, y-axis", nil, true})
assert Param.to_human(:movement_home_at_boot_y, 1) == t(:movement_keep_active_x, 1, {"always power motors, x-axis", nil, true})
{"find home on boot, y-axis", nil, true} t(:movement_invert_motor_z, 1, {"invert motor, z-axis", nil, true})
t(:movement_invert_motor_y, 1, {"invert motor, y-axis", nil, true})
assert Param.to_human(:movement_home_at_boot_z, 1) == t(:movement_invert_motor_x, 1, {"invert motor, x-axis", nil, true})
{"find home on boot, z-axis", nil, true} t(:movement_invert_endpoints_z, 1, {"swap endstops, z-axis", nil, true})
t(:movement_invert_endpoints_y, 1, {"swap endstops, y-axis", nil, true})
assert Param.to_human(:movement_invert_endpoints_x, 1) == t(:movement_invert_endpoints_x, 1, {"swap endstops, x-axis", nil, true})
{"swap endstops, x-axis", nil, true} t(:movement_home_at_boot_z, 1, {"find home on boot, z-axis", nil, true})
t(:movement_home_at_boot_y, 1, {"find home on boot, y-axis", nil, true})
assert Param.to_human(:movement_invert_endpoints_y, 1) == t(:movement_home_at_boot_x, 1, {"find home on boot, x-axis", nil, true})
{"swap endstops, y-axis", nil, true} t(:movement_enable_endpoints_z, 1, {"enable endstops, z-axis", nil, true})
t(:movement_enable_endpoints_y, 1, {"enable endstops, y-axis", nil, true})
assert Param.to_human(:movement_invert_endpoints_z, 1) == t(:movement_enable_endpoints_x, 1, {"enable endstops, x-axis", nil, true})
{"swap endstops, z-axis", nil, true} t(:encoder_type_z, 1.2, {"encoder type, z-axis", nil, "1.2"})
t(:encoder_type_y, 1.2, {"encoder type, y-axis", nil, "1.2"})
assert Param.to_human(:movement_enable_endpoints_x, 1) == t(:encoder_type_x, 1.2, {"encoder type, x-axis", nil, "1.2"})
{"enable endstops, x-axis", nil, true} t(:encoder_invert_z, 1, {"invert encoders, z-axis", nil, true})
t(:encoder_invert_y, 1, {"invert encoders, y-axis", nil, true})
assert Param.to_human(:movement_enable_endpoints_y, 1) == t(:encoder_invert_x, 1, {"invert encoders, x-axis", nil, true})
{"enable endstops, y-axis", nil, true}
t(
assert Param.to_human(:movement_enable_endpoints_z, 1) == :movement_motor_current_x,
{"enable endstops, z-axis", nil, true} float_value,
{"motor current, x-axis", "(milliamps)", "1.2"}
assert Param.to_human(:movement_invert_motor_x, 1) == )
{"invert motor, x-axis", nil, true}
t(
assert Param.to_human(:movement_invert_motor_y, 1) == :movement_motor_current_y,
{"invert motor, y-axis", nil, true} float_value,
{"motor current, y-axis", "(milliamps)", "1.2"}
assert Param.to_human(:movement_invert_motor_z, 1) == )
{"invert motor, z-axis", nil, true}
t(
assert Param.to_human(:movement_secondary_motor_x, 1) == :movement_motor_current_z,
{"enable 2nd x motor", nil, true} float_value,
{"motor current, z-axis", "(milliamps)", "1.2"}
assert Param.to_human(:movement_secondary_motor_invert_x, 1) == )
{"invert 2nd x motor", nil, true}
t(
assert Param.to_human(:movement_stop_at_home_x, 1) == :movement_stall_sensitivity_x,
{"stop at home, x-axis", nil, true} float_value,
{"stall sensitivity, x-axis", nil, "1.2"}
assert Param.to_human(:movement_stop_at_home_y, 1) == )
{"stop at home, y-axis", nil, true}
t(
assert Param.to_human(:movement_stop_at_home_z, 1) == :movement_stall_sensitivity_y,
{"stop at home, z-axis", nil, true} float_value,
{"stall sensitivity, y-axis", nil, "1.2"}
assert Param.to_human(:movement_step_per_mm_x, float_value) == )
{"steps per mm, x-axis", steps_per_mm, "1.2"}
t(
assert Param.to_human(:movement_step_per_mm_y, float_value) == :movement_stall_sensitivity_z,
{"steps per mm, y-axis", steps_per_mm, "1.2"} float_value,
{"stall sensitivity, z-axis", nil, "1.2"}
assert Param.to_human(:movement_step_per_mm_z, float_value) == )
{"steps per mm, z-axis", steps_per_mm, "1.2"}
t(
assert Param.to_human(:movement_min_spd_x, float_value) == :movement_timeout_x,
{"minimum speed, x-axis", steps_per_s, "1.2"} float_value,
{"timeout after, x-axis", seconds, "1.2"}
assert Param.to_human(:movement_min_spd_y, float_value) == )
{"minimum speed, y-axis", steps_per_s, "1.2"}
t(
assert Param.to_human(:movement_min_spd_z, float_value) == :movement_timeout_y,
{"minimum speed, z-axis", steps_per_s, "1.2"} float_value,
{"timeout after, y-axis", seconds, "1.2"}
assert Param.to_human(:movement_home_spd_x, float_value) == )
{"homing speed, x-axis", steps_per_s, "1.2"}
t(
assert Param.to_human(:movement_home_spd_y, float_value) == :movement_timeout_z,
{"homing speed, y-axis", steps_per_s, "1.2"} float_value,
{"timeout after, z-axis", seconds, "1.2"}
assert Param.to_human(:movement_home_spd_z, float_value) == )
{"homing speed, z-axis", steps_per_s, "1.2"}
t(
assert Param.to_human(:movement_max_spd_x, float_value) == :movement_step_per_mm_x,
{"max speed, x-axis", steps_per_s, "1.2"} float_value,
{"steps per mm, x-axis", steps_per_mm, "1.2"}
assert Param.to_human(:movement_max_spd_y, float_value) == )
{"max speed, y-axis", steps_per_s, "1.2"}
t(
assert Param.to_human(:movement_max_spd_z, float_value) == :movement_step_per_mm_y,
{"max speed, z-axis", steps_per_s, "1.2"} float_value,
{"steps per mm, y-axis", steps_per_mm, "1.2"}
assert Param.to_human(:movement_invert_2_endpoints_x, 1) == )
{"invert endstops, x-axis", nil, true}
t(
assert Param.to_human(:movement_invert_2_endpoints_y, 1) == :movement_step_per_mm_z,
{"invert endstops, y-axis", nil, true} float_value,
{"steps per mm, z-axis", steps_per_mm, "1.2"}
assert Param.to_human(:movement_invert_2_endpoints_z, 1) == )
{"invert endstops, z-axis", nil, true}
t(
assert Param.to_human(:encoder_enabled_x, 1) == :movement_min_spd_x,
{"enable encoders / stall detection, x-axis", nil, true} float_value,
{"minimum speed, x-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_enabled_y, 1) == )
{"enable encoders / stall detection, y-axis", nil, true}
t(
assert Param.to_human(:encoder_enabled_z, 1) == :movement_min_spd_y,
{"enable encoders / stall detection, z-axis", nil, true} float_value,
{"minimum speed, y-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_type_x, float_value) == )
{"encoder type, x-axis", nil, "1.2"}
t(
assert Param.to_human(:encoder_type_y, float_value) == :movement_min_spd_z,
{"encoder type, y-axis", nil, "1.2"} float_value,
{"minimum speed, z-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_type_z, float_value) == )
{"encoder type, z-axis", nil, "1.2"}
t(
assert Param.to_human(:encoder_scaling_x, float_value) == :movement_home_spd_x,
{"encoder scaling, x-axis", nil, "1.2"} float_value,
{"homing speed, x-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_scaling_y, float_value) == )
{"encoder scaling, y-axis", nil, "1.2"}
t(
assert Param.to_human(:encoder_scaling_z, float_value) == :movement_home_spd_y,
{"encoder scaling, z-axis", nil, "1.2"} float_value,
{"homing speed, y-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_missed_steps_decay_x, float_value) == )
{"missed step decay, x-axis", steps, "1.2"}
t(
assert Param.to_human(:encoder_missed_steps_decay_y, float_value) == :movement_home_spd_z,
{"missed step decay, y-axis", steps, "1.2"} float_value,
{"homing speed, z-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_missed_steps_decay_z, float_value) == )
{"missed step decay, z-axis", steps, "1.2"}
t(
assert Param.to_human(:encoder_use_for_pos_x, 1) == :movement_max_spd_x,
{"use encoders for positioning, x-axis", nil, true} float_value,
{"max speed, x-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_use_for_pos_y, 1) == )
{"use encoders for positioning, y-axis", nil, true}
t(
assert Param.to_human(:encoder_use_for_pos_z, 1) == :movement_max_spd_y,
{"use encoders for positioning, z-axis", nil, true} float_value,
{"max speed, y-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_invert_x, 1) == )
{"invert encoders, x-axis", nil, true}
t(
assert Param.to_human(:encoder_invert_y, 1) == :movement_max_spd_z,
{"invert encoders, y-axis", nil, true} float_value,
{"max speed, z-axis", steps_per_s, "1.2"}
assert Param.to_human(:encoder_invert_z, 1) == )
{"invert encoders, z-axis", nil, true}
t(
assert Param.to_human(:movement_stop_at_max_x, 1) == :movement_invert_2_endpoints_x,
{"stop at max, x-axis", nil, true} 1,
{"invert endstops, x-axis", nil, true}
assert Param.to_human(:movement_stop_at_max_y, 1) == )
{"stop at max, y-axis", nil, true}
t(
assert Param.to_human(:movement_stop_at_max_z, 1) == :movement_invert_2_endpoints_y,
{"stop at max, z-axis", nil, true} 1,
{"invert endstops, y-axis", nil, true}
assert Param.to_human(:pin_guard_1_active_state, 0) == )
{"pin guard 1 safe state", nil, "HIGH"}
t(
assert Param.to_human(:pin_guard_2_active_state, 0) == :movement_invert_2_endpoints_z,
{"pin guard 2 safe state", nil, "HIGH"} 1,
{"invert endstops, z-axis", nil, true}
assert Param.to_human(:pin_guard_3_active_state, 0) == )
{"pin guard 3 safe state", nil, "HIGH"}
t(
assert Param.to_human(:pin_guard_4_active_state, 0) == :encoder_enabled_x,
{"pin guard 4 safe state", nil, "HIGH"} 1,
{"enable encoders / stall detection, x-axis", nil, true}
assert Param.to_human(:pin_guard_5_active_state, 0) == )
{"pin guard 5 safe state", nil, "HIGH"}
t(
:encoder_enabled_y,
1,
{"enable encoders / stall detection, y-axis", nil, true}
)
t(
:encoder_enabled_z,
1,
{"enable encoders / stall detection, z-axis", nil, true}
)
t(
:encoder_scaling_x,
float_value,
{"encoder scaling, x-axis", nil, "1.2"}
)
t(
:encoder_scaling_y,
float_value,
{"encoder scaling, y-axis", nil, "1.2"}
)
t(
:encoder_scaling_z,
float_value,
{"encoder scaling, z-axis", nil, "1.2"}
)
t(
:encoder_missed_steps_decay_x,
float_value,
{"missed step decay, x-axis", steps, "1.2"}
)
t(
:encoder_missed_steps_decay_y,
float_value,
{"missed step decay, y-axis", steps, "1.2"}
)
t(
:encoder_missed_steps_decay_z,
float_value,
{"missed step decay, z-axis", steps, "1.2"}
)
t(
:encoder_use_for_pos_x,
1,
{"use encoders for positioning, x-axis", nil, true}
)
t(
:encoder_use_for_pos_y,
1,
{"use encoders for positioning, y-axis", nil, true}
)
t(
:encoder_use_for_pos_z,
1,
{"use encoders for positioning, z-axis", nil, true}
)
t(
:movement_axis_nr_steps_z,
1.0,
{"axis length, z-axis", "(steps)", "1"}
)
t(
:movement_axis_nr_steps_y,
1.0,
{"axis length, y-axis", "(steps)", "1"}
)
t(
:movement_axis_nr_steps_x,
1.0,
{"axis length, x-axis", "(steps)", "1"}
)
t(
:movement_steps_acc_dec_x,
1.0,
{"accelerate for, x-axis", "(steps)", "1"}
)
t(
:movement_steps_acc_dec_y,
1.0,
{"accelerate for, y-axis", "(steps)", "1"}
)
t(
:movement_steps_acc_dec_z,
1.0,
{"accelerate for, z-axis", "(steps)", "1"}
)
t(
:movement_home_up_x,
1.0,
{"negative coordinates only, x-axis", nil, true}
)
t(
:movement_home_up_y,
1.0,
{"negative coordinates only, y-axis", nil, true}
)
t(
:movement_home_up_z,
1.0,
{"negative coordinates only, z-axis", nil, true}
)
t(
:encoder_missed_steps_max_x,
1.0,
{"max missed steps, x-axis", "(steps)", "1"}
)
t(
:encoder_missed_steps_max_y,
1.0,
{"max missed steps, y-axis", "(steps)", "1"}
)
t(
:encoder_missed_steps_max_z,
1.0,
{"max missed steps, z-axis", "(steps)", "1"}
)
end end
test "Handling of uknown parameters" do test "Handling of uknown parameters" do
assert :unknown_parameter == Param.decode(-999) log =
capture_log(fn ->
assert :unknown_parameter == Param.decode(-999)
end)
assert log =~ "unknown firmware parameter: -999"
end end
end end

View File

@ -100,9 +100,7 @@ if Mix.target() == :host do
else else
import_config("target/#{Mix.env()}.exs") import_config("target/#{Mix.env()}.exs")
if File.exists?("config/target/#{Mix.target()}.exs") do import_config("target/#{Mix.target()}.exs")
import_config("target/#{Mix.target()}.exs")
end
end end
if Mix.env() == :test do if Mix.env() == :test do

View File

@ -43,3 +43,5 @@ config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 0 firmware_flash_attempt_threshold: 0
config :plug, :validate_header_keys_during_test, true config :plug, :validate_header_keys_during_test, true
config :ex_unit, capture_logs: true

View File

@ -114,13 +114,6 @@ config :farmbot, FarmbotOS.Configurator,
config :farmbot, FarmbotOS.System, config :farmbot, FarmbotOS.System,
system_tasks: FarmbotOS.Platform.Target.SystemTasks system_tasks: FarmbotOS.Platform.Target.SystemTasks
config :nerves_hub,
client: FarmbotOS.Platform.Target.NervesHubClient,
remote_iex: true,
public_keys: [File.read!("priv/staging.pub"), File.read!("priv/prod.pub")]
config :nerves_hub, NervesHub.Socket, reconnect_interval: 5_000
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5 config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,

View File

@ -10,3 +10,7 @@ config :farmbot, FarmbotOS.Init.Supervisor,
init_children: [ init_children: [
FarmbotOS.Platform.Target.RTCWorker FarmbotOS.Platform.Target.RTCWorker
] ]
# :farmbot_firmware, FarmbotFirmware changes too much.
# Needed one that would stay stable, so I duplicated it here:
config :farmbot, FarmbotOS.SysCalls.FlashFirmware, gpio: Circuits.GPIO

View File

@ -8,6 +8,10 @@ config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 50
config :farmbot_firmware, FarmbotFirmware, config :farmbot_firmware, FarmbotFirmware,
reset: FarmbotOS.Platform.Target.FirmwareReset.GPIO reset: FarmbotOS.Platform.Target.FirmwareReset.GPIO
# :farmbot_firmware, FarmbotFirmware changes too much.
# Needed one that would stay stable, so I duplicated it here:
config :farmbot, FarmbotOS.SysCalls.FlashFirmware, gpio: Circuits.GPIO
config :farmbot, FarmbotOS.Init.Supervisor, config :farmbot, FarmbotOS.Init.Supervisor,
init_children: [ init_children: [
FarmbotOS.Platform.Target.RTCWorker FarmbotOS.Platform.Target.RTCWorker

View File

@ -17,7 +17,6 @@ defmodule Avrdude do
_ = File.stat!(hex_path) _ = File.stat!(hex_path)
# STEP 1: Is the UART in use?
args = [ args = [
"-patmega2560", "-patmega2560",
"-cwiring", "-cwiring",
@ -25,16 +24,23 @@ defmodule Avrdude do
"-b#{@uart_speed}", "-b#{@uart_speed}",
"-D", "-D",
"-V", "-V",
"-v",
"-Uflash:w:#{hex_path}:i" "-Uflash:w:#{hex_path}:i"
] ]
# call the function for resetting the line before executing avrdude. FarmbotCore.Logger.info(3, "Writing firmware to MCU...")
call_reset_fun(reset_fun) call_reset_fun(reset_fun)
MuonTrap.cmd("avrdude", args, result = MuonTrap.cmd("avrdude", args, stderr_to_stdout: true)
into: IO.stream(:stdio, :line),
stderr_to_stdout: true if is_tuple(result) do
) {a, exit_code} = result
FarmbotCore.Logger.info(3, inspect(a))
FarmbotCore.Logger.info(3, "Exit code #{exit_code}")
end
result
end end
def call_reset_fun(reset_fun) do def call_reset_fun(reset_fun) do

View File

@ -20,16 +20,7 @@ defmodule FarmbotOS.Configurator.LoggerSocket do
end end
@impl :cowboy_websocket @impl :cowboy_websocket
def websocket_handle({:text, message}, state) do def websocket_handle({:text, _}, state), do: {:ok, state}
case Jason.decode(message) do
{:ok, json} ->
websocket_handle({:json, json}, state)
_ ->
_ = Logger.debug("discarding info: #{message}")
{:ok, state}
end
end
@impl :cowboy_websocket @impl :cowboy_websocket
def websocket_info(:after_connect, state) do def websocket_info(:after_connect, state) do

View File

@ -1,6 +1,6 @@
defmodule FarmbotOS.Lua.Ext.Firmware do defmodule FarmbotOS.Lua.Ext.Firmware do
@moduledoc """ @moduledoc """
Lua extensions for interacting with the Firmware Lua extensions for interacting with the Firmware
""" """
alias FarmbotCeleryScript.SysCalls alias FarmbotCeleryScript.SysCalls

View File

@ -4,7 +4,7 @@ defmodule FarmbotOS.SysCalls.Farmware do
require FarmbotCore.Logger require FarmbotCore.Logger
alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime} alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime}
alias FarmbotExt.API.ImageUploader alias FarmbotExt.API.ImageUploader
@farmware_timeout 60_000 @farmware_timeout 1_200_000
def update_farmware(farmware_name) do def update_farmware(farmware_name) do
with {:ok, installation} <- lookup_installation(farmware_name) do with {:ok, installation} <- lookup_installation(farmware_name) do
@ -56,9 +56,9 @@ defmodule FarmbotOS.SysCalls.Farmware do
end end
def farmware_timeout(farmware_runtime) do def farmware_timeout(farmware_runtime) do
time = @farmware_timeout / 1_000 time = @farmware_timeout / 1_000 / 60
runtime = inspect(farmware_runtime) runtime = inspect(farmware_runtime)
msg = "Farmware did not exit after #{time} seconds. Terminating #{runtime}" msg = "Farmware did not exit after #{time} minutes. Terminating #{runtime}"
FarmbotCore.Logger.info(2, msg) FarmbotCore.Logger.info(2, msg)
FarmwareRuntime.stop(farmware_runtime) FarmwareRuntime.stop(farmware_runtime)

View File

@ -1,3 +1,5 @@
Application.get_env(:farmbot, FarmbotOS.SysCalls.FlashFirmware, [])[:gpio]
defmodule FarmbotOS.SysCalls.FlashFirmware do defmodule FarmbotOS.SysCalls.FlashFirmware do
@moduledoc false @moduledoc false
@ -5,6 +7,23 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
alias FarmbotFirmware alias FarmbotFirmware
alias FarmbotCore.FirmwareTTYDetector alias FarmbotCore.FirmwareTTYDetector
defmodule Stub do
require FarmbotCore.Logger
def fail do
m = "No express function found. Please notify support."
FarmbotCore.Logger.error(3, m)
{:error, m}
end
def open(_, _), do: fail()
def write(_, _), do: fail()
end
# This only matter for express.
# When it's an express, use Circuits.GPIO.
@gpio Application.get_env(:farmbot, __MODULE__, [])[:gpio] || Stub
import FarmbotFirmware.PackageUtils, import FarmbotFirmware.PackageUtils,
only: [find_hex_file: 1, package_to_string: 1] only: [find_hex_file: 1, package_to_string: 1]
@ -29,9 +48,7 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
), ),
:ok <- FarmbotFirmware.close_transport(), :ok <- FarmbotFirmware.close_transport(),
_ <- FarmbotCore.Logger.debug(3, "starting firmware flash"), _ <- FarmbotCore.Logger.debug(3, "starting firmware flash"),
{_, 0} <- Avrdude.flash(hex_file, tty, fun) do _ <- finish_flashing(Avrdude.flash(hex_file, tty, fun)) do
FarmbotCore.Logger.success(2, "Firmware flashed successfully!")
%{firmware_path: tty} %{firmware_path: tty}
|> Asset.update_fbos_config!() |> Asset.update_fbos_config!()
|> Private.mark_dirty!(%{}) |> Private.mark_dirty!(%{})
@ -42,10 +59,18 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
{:error, reason} {:error, reason}
error -> error ->
{:error, "flash_firmware misc error: #{inspect(error)}"} {:error, "flash_firmware returned #{inspect(error)}"}
end end
end end
def finish_flashing({_result, 0}) do
FarmbotCore.Logger.success(2, "Firmware flashed successfully!")
end
def finish_flashing(result) do
FarmbotCore.Logger.debug(2, "AVR flash returned #{inspect(result)}")
end
defp find_tty() do defp find_tty() do
case FirmwareTTYDetector.tty() do case FirmwareTTYDetector.tty() do
nil -> nil ->
@ -60,18 +85,30 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
end end
defp find_reset_fun("express_k10") do defp find_reset_fun("express_k10") do
FarmbotCore.Logger.debug(3, "Using special reset function for express") FarmbotCore.Logger.debug(3, "Using special express reset function")
# "magic" workaround to avoid compiler warnings. {:ok, fn -> express_reset_fun() end}
# We used to inject this via App config, but it was
# error prone.
mod = :"Elixir.FarmbotOS.Platform.Target.FirmwareReset.GPIO"
fun = &mod.reset/0
{:ok, fun}
end end
defp find_reset_fun(_) do defp find_reset_fun(_) do
FarmbotCore.Logger.debug(3, "Using default reset function") FarmbotCore.Logger.debug(3, "Using default reset function")
fun = &FarmbotFirmware.NullReset.reset/0 {:ok, &FarmbotFirmware.NullReset.reset/0}
{:ok, fun} end
def express_reset_fun() do
try do
FarmbotCore.Logger.debug(3, "Begin MCU reset")
{:ok, gpio} = @gpio.open(19, :output)
:ok = @gpio.write(gpio, 0)
:ok = @gpio.write(gpio, 1)
Process.sleep(1000)
:ok = @gpio.write(gpio, 0)
FarmbotCore.Logger.debug(3, "Finish MCU Reset")
:ok
rescue
ex ->
message = Exception.message(ex)
Logger.error("Could not flash express firmware: #{message}")
:express_reset_error
end
end end
end end

View File

@ -23,7 +23,7 @@ defmodule FarmbotOS.Platform.Target.PinBindingWorker.CircuitsGPIOHandler do
{:ok, pin} = GPIO.open(pin_number, :input) {:ok, pin} = GPIO.open(pin_number, :input)
:ok = GPIO.set_interrupts(pin, :rising) :ok = GPIO.set_interrupts(pin, :rising)
# this has been checked on v1.3 and v1.5 hardware # this has been checked on v1.3 and v1.5 hardware
# and it seems to be fine. # and it seems to be fine.
:ok = GPIO.set_pull_mode(pin, :pulldown) :ok = GPIO.set_pull_mode(pin, :pulldown)
{:ok, %{pin_number: pin_number, pin: pin, fun: fun, debounce: nil}} {:ok, %{pin_number: pin_number, pin: pin, fun: fun, debounce: nil}}
end end

View File

@ -31,17 +31,11 @@ defmodule FarmbotOs.AvrdudeTest do
"-b115200", "-b115200",
"-D", "-D",
"-V", "-V",
"-v",
"-Uflash:w:/tmp/wow:i" "-Uflash:w:/tmp/wow:i"
] ]
assert opts == [ assert opts == [stderr_to_stdout: true]
into: %IO.Stream{
device: :standard_io,
line_or_bytes: :line,
raw: false
},
stderr_to_stdout: true
]
end) end)
Avrdude.flash("/tmp/wow", "null", fn -> Avrdude.flash("/tmp/wow", "null", fn ->
@ -62,17 +56,11 @@ defmodule FarmbotOs.AvrdudeTest do
"-b115200", "-b115200",
"-D", "-D",
"-V", "-V",
"-v",
"-Uflash:w:/tmp/wow:i" "-Uflash:w:/tmp/wow:i"
] ]
assert opts == [ assert opts == [stderr_to_stdout: true]
into: %IO.Stream{
device: :standard_io,
line_or_bytes: :line,
raw: false
},
stderr_to_stdout: true
]
end) end)
Avrdude.flash("/tmp/wow", "/dev/null", fn -> Avrdude.flash("/tmp/wow", "/dev/null", fn ->

View File

@ -3,12 +3,28 @@ defmodule FarmbotOS.Configurator.LoggerSocketTest do
use Mimic use Mimic
alias FarmbotOS.Configurator.LoggerSocket alias FarmbotOS.Configurator.LoggerSocket
setup :verify_on_exit! setup :verify_on_exit!
import ExUnit.CaptureLog
test "init/2" do test "init/2" do
# TODO(Rick) Not sure what the real args are.
# Circle back to make this test more realistic
# later.
expected = {:cowboy_websocket, :foo, :bar} expected = {:cowboy_websocket, :foo, :bar}
assert expected == LoggerSocket.init(:foo, :bar) assert expected == LoggerSocket.init(:foo, :bar)
end end
test "websocket_init" do
assert {:ok, %{}} == LoggerSocket.websocket_init(nil)
assert_receive :after_connect
end
test "websocket_handle (invalid JSON)" do
s = %{state: :yep}
msg = "Not JSON."
payl = {:text, msg}
assert {:ok, s} == LoggerSocket.websocket_handle(payl, s)
end
test "websocket_info/2" do
assert capture_log(fn ->
LoggerSocket.websocket_info(:whatever, %{})
end) =~ "Dropping :whatever"
end
end end

View File

@ -8,6 +8,8 @@ defmodule FarmbotOS.Configurator.RouterTest do
use Mimic use Mimic
setup :verify_on_exit! setup :verify_on_exit!
import ExUnit.CaptureIO
@opts Router.init([]) @opts Router.init([])
# Stolen from https://github.com/phoenixframework/phoenix/blob/3f157c30ceae8d1eb524fdd05b5e3de10e434c42/lib/phoenix/test/conn_test.ex#L438 # Stolen from https://github.com/phoenixframework/phoenix/blob/3f157c30ceae8d1eb524fdd05b5e3de10e434c42/lib/phoenix/test/conn_test.ex#L438
defp redirected_to(conn, status \\ 302) defp redirected_to(conn, status \\ 302)
@ -34,6 +36,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
Router.call(conn, @opts) Router.call(conn, @opts)
end end
@tag :capture_log
test "index after reset" do test "index after reset" do
FarmbotOS.Configurator.ConfigDataLayer FarmbotOS.Configurator.ConfigDataLayer
|> expect(:load_last_reset_reason, fn -> "whoops!" end) |> expect(:load_last_reset_reason, fn -> "whoops!" end)
@ -45,6 +48,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "whoops!" assert conn.resp_body =~ "whoops!"
end end
@tag :capture_log
test "redirects" do test "redirects" do
redirects = [ redirects = [
"/check_network_status.txt", "/check_network_status.txt",
@ -66,6 +70,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
end) end)
end end
@tag :capture_log
test "celeryscript requests don't get listed as last reset reason" do test "celeryscript requests don't get listed as last reset reason" do
FarmbotOS.Configurator.ConfigDataLayer FarmbotOS.Configurator.ConfigDataLayer
|> expect(:load_last_reset_reason, fn -> "CeleryScript request." end) |> expect(:load_last_reset_reason, fn -> "CeleryScript request." end)
@ -75,6 +80,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
refute conn.resp_body =~ "CeleryScript request." refute conn.resp_body =~ "CeleryScript request."
end end
@tag :capture_log
test "no reset reason" do test "no reset reason" do
FarmbotOS.Configurator.ConfigDataLayer FarmbotOS.Configurator.ConfigDataLayer
|> expect(:load_last_reset_reason, fn -> nil end) |> expect(:load_last_reset_reason, fn -> nil end)
@ -84,6 +90,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
refute conn.resp_body =~ "<div class=\"last-shutdown-reason\">" refute conn.resp_body =~ "<div class=\"last-shutdown-reason\">"
end end
@tag :capture_log
test "captive portal" do test "captive portal" do
conn = conn(:get, "/generate_204") conn = conn(:get, "/generate_204")
conn = Router.call(conn, @opts) conn = Router.call(conn, @opts)
@ -94,6 +101,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.status == 302 assert conn.status == 302
end end
@tag :capture_log
test "network index" do test "network index" do
FarmbotOS.Configurator.FakeNetworkLayer FarmbotOS.Configurator.FakeNetworkLayer
|> expect(:list_interfaces, fn -> |> expect(:list_interfaces, fn ->
@ -108,6 +116,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "eth0" assert conn.resp_body =~ "eth0"
end end
@tag :capture_log
test "select network sets session data" do test "select network sets session data" do
conn = conn(:post, "select_interface") conn = conn(:post, "select_interface")
conn = Router.call(conn, @opts) conn = Router.call(conn, @opts)
@ -124,6 +133,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert get_session(conn, "ifname") == "wlan0" assert get_session(conn, "ifname") == "wlan0"
end end
@tag :capture_log
test "config wired" do test "config wired" do
conn = conn =
conn(:get, "/config_wired") conn(:get, "/config_wired")
@ -133,6 +143,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "Advanced settings" assert conn.resp_body =~ "Advanced settings"
end end
@tag :capture_log
test "config wireless SSID list" do test "config wireless SSID list" do
FarmbotOS.Configurator.FakeNetworkLayer FarmbotOS.Configurator.FakeNetworkLayer
|> expect(:scan, fn _ -> |> expect(:scan, fn _ ->
@ -154,6 +165,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "Test Network" assert conn.resp_body =~ "Test Network"
end end
@tag :capture_log
test "config wireless" do test "config wireless" do
# No SSID or SECURITY # No SSID or SECURITY
conn = conn =
@ -238,6 +250,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "unknown or unsupported" assert conn.resp_body =~ "unknown or unsupported"
end end
@tag :capture_log
test "config_network" do test "config_network" do
params = %{ params = %{
"dns_name" => "super custom", "dns_name" => "super custom",
@ -290,6 +303,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert redirected_to(conn) == "/credentials" assert redirected_to(conn) == "/credentials"
end end
@tag :capture_log
test "credentials index" do test "credentials index" do
FarmbotOS.Configurator.ConfigDataLayer FarmbotOS.Configurator.ConfigDataLayer
|> expect(:load_email, fn -> "test@test.org" end) |> expect(:load_email, fn -> "test@test.org" end)
@ -302,6 +316,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "https://my.farm.bot" assert conn.resp_body =~ "https://my.farm.bot"
end end
@tag :capture_log
test "configure credentials" do test "configure credentials" do
params = %{ params = %{
"email" => "test@test.org", "email" => "test@test.org",
@ -334,6 +349,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert redirected_to(conn) == "/credentials" assert redirected_to(conn) == "/credentials"
end end
@tag :capture_log
test "finish" do test "finish" do
conn = conn =
conn(:get, "/finish") conn(:get, "/finish")
@ -342,6 +358,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
end end
@tag :capture_log
test "404" do test "404" do
conn = conn =
conn(:get, "/whoops") conn(:get, "/whoops")
@ -350,6 +367,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body == "Page not found" assert conn.resp_body == "Page not found"
end end
@tag :capture_log
test "500" do test "500" do
FarmbotOS.Configurator.FakeNetworkLayer FarmbotOS.Configurator.FakeNetworkLayer
|> expect(:scan, fn _ -> |> expect(:scan, fn _ ->
@ -360,20 +378,26 @@ defmodule FarmbotOS.Configurator.RouterTest do
] ]
end) end)
conn = crasher = fn ->
conn(:get, "/config_wireless") conn =
|> init_test_session(%{"ifname" => "wlan0"}) conn(:get, "/config_wireless")
|> Router.call(@opts) |> init_test_session(%{"ifname" => "wlan0"})
|> Router.call(@opts)
assert conn.status == 500 assert conn.status == 500
end
assert capture_io(:stderr, crasher) =~ "render error"
end end
@tag :capture_log
test "/scheduler_debugger" do test "/scheduler_debugger" do
kon = get_con("/scheduler_debugger") kon = get_con("/scheduler_debugger")
assert String.contains?(kon.resp_body, "scheduler_debugger.js") assert String.contains?(kon.resp_body, "scheduler_debugger.js")
assert String.contains?(kon.resp_body, "<title>Scheduler Debugger</title>") assert String.contains?(kon.resp_body, "<title>Scheduler Debugger</title>")
end end
@tag :capture_log
test "/logger" do test "/logger" do
kon = get_con("/logger") kon = get_con("/logger")
@ -391,6 +415,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
end) end)
end end
@tag :capture_log
test "/api/telemetry/cpu_usage" do test "/api/telemetry/cpu_usage" do
{:ok, json} = Jason.decode(get_con("/api/telemetry/cpu_usage").resp_body) {:ok, json} = Jason.decode(get_con("/api/telemetry/cpu_usage").resp_body)
assert Enum.count(json) == 10 assert Enum.count(json) == 10
@ -400,6 +425,7 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert(is_integer(zero["value"])) assert(is_integer(zero["value"]))
end end
@tag :capture_log
test "/finish" do test "/finish" do
expect(ConfigDataLayer, :save_config, 1, fn _conf -> expect(ConfigDataLayer, :save_config, 1, fn _conf ->
:ok :ok

View File

@ -0,0 +1,75 @@
defmodule FarmbotOS.Lua.Ext.FirmwareTest do
alias FarmbotOS.Lua.Ext.Firmware
use ExUnit.Case
use Mimic
setup :verify_on_exit!
test "calibrate/2" do
msg = "expected stub error"
lua = "return"
expect(FarmbotCeleryScript.SysCalls, :calibrate, 2, fn
"x" -> :ok
_ -> {:error, msg}
end)
assert {[true], ^lua} = Firmware.calibrate(["x"], lua)
assert {[nil, ^msg], ^lua} = Firmware.calibrate(["y"], lua)
end
test "move_absolute/2" do
msg = "expected stub error"
lua = "return"
expect(FarmbotCeleryScript.SysCalls, :move_absolute, 4, fn
1, _, _, _ -> :ok
_, _, _, _ -> {:error, msg}
end)
assert {[true], ^lua} = Firmware.move_absolute([1, 2, 3, 4], lua)
assert {[nil, ^msg], ^lua} = Firmware.move_absolute([5, 6, 7, 8], lua)
assert {[true], ^lua} = Firmware.move_absolute([1, 2, 3], lua)
assert {[nil, ^msg], ^lua} = Firmware.move_absolute([5, 6, 7], lua)
end
test "find_home/2" do
msg = "expected stub error"
lua = "return"
expect(FarmbotCeleryScript.SysCalls, :find_home, 2, fn
"x" -> :ok
_ -> {:error, msg}
end)
assert {[true], ^lua} = Firmware.find_home(["x"], lua)
assert {[nil, ^msg], ^lua} = Firmware.find_home(["y"], lua)
end
test "emergency_lock/2" do
msg = "expected stub error"
lua = "return"
expect(FarmbotCeleryScript.SysCalls, :emergency_lock, 1, fn -> :ok end)
assert {[true], ^lua} = Firmware.emergency_lock(:ok, lua)
expect(FarmbotCeleryScript.SysCalls, :emergency_lock, 1, fn ->
{:error, msg}
end)
assert {[nil, ^msg], ^lua} = Firmware.emergency_lock(nil, lua)
end
test "emergency_unlock/2" do
msg = "expected stub error"
lua = "return"
expect(FarmbotCeleryScript.SysCalls, :emergency_unlock, 1, fn -> :ok end)
assert {[true], ^lua} = Firmware.emergency_unlock(:ok, lua)
expect(FarmbotCeleryScript.SysCalls, :emergency_unlock, 1, fn ->
{:error, msg}
end)
assert {[nil, ^msg], ^lua} = Firmware.emergency_unlock(nil, lua)
end
end

View File

@ -4,6 +4,7 @@ defmodule FarmbotOS.LuaTest do
setup :verify_on_exit! setup :verify_on_exit!
alias FarmbotOS.Lua alias FarmbotOS.Lua
@tag :capture_log
test "evaluates Lua" do test "evaluates Lua" do
assert Lua.eval_assertion("Returns 'true'", "return true") assert Lua.eval_assertion("Returns 'true'", "return true")
{:error, message1} = Lua.eval_assertion("Returns 'true'", "-1") {:error, message1} = Lua.eval_assertion("Returns 'true'", "-1")

View File

@ -11,6 +11,7 @@ defmodule FarmbotOS.SysCallsTest do
use Mimic use Mimic
setup :verify_on_exit! setup :verify_on_exit!
import ExUnit.CaptureIO
test "emergency_unlock" do test "emergency_unlock" do
expect(FarmbotFirmware, :command, fn {:command_emergency_unlock, []} -> expect(FarmbotFirmware, :command, fn {:command_emergency_unlock, []} ->
@ -71,6 +72,7 @@ defmodule FarmbotOS.SysCallsTest do
assert {:error, "Could not find peripheral by id: 11"} == result6 assert {:error, "Could not find peripheral by id: 11"} == result6
end end
@tag :capture_log
test "sync() success" do test "sync() success" do
# Expect 5 calls and an :ok response. # Expect 5 calls and an :ok response.
expect(FarmbotExt.API.Reconciler, :sync_group, 5, fn changeset, _group -> expect(FarmbotExt.API.Reconciler, :sync_group, 5, fn changeset, _group ->
@ -81,16 +83,21 @@ defmodule FarmbotOS.SysCallsTest do
{:ok, %{wut: module}} {:ok, %{wut: module}}
end) end)
assert :ok == SysCalls.sync() assert capture_io(fn ->
assert :ok == SysCalls.sync()
end) =~ "green really_fast_blink"
end end
@tag :capture_log
test "sync() failure" do test "sync() failure" do
# Expect 5 calls and an :ok response. # Expect 5 calls and an :ok response.
expect(FarmbotExt.API, :get_changeset, fn FarmbotCore.Asset.Sync -> expect(FarmbotExt.API, :get_changeset, fn FarmbotCore.Asset.Sync ->
"this is a test" "this is a test"
end) end)
assert {:error, "\"this is a test\""} == SysCalls.sync() assert capture_io(fn ->
assert {:error, "\"this is a test\""} == SysCalls.sync()
end) =~ "green slow_blink"
end end
test "get_sequence(id)" do test "get_sequence(id)" do

View File

@ -9,7 +9,7 @@ defmodule FarmbotOS.SysCalls.FarmwareTest do
expect(FarmbotCore.LogExecutor, :execute, fn log -> expect(FarmbotCore.LogExecutor, :execute, fn log ->
expected = expected =
"Farmware did not exit after 60.0 seconds. Terminating :FAKE_PID" "Farmware did not exit after 20.0 minutes. Terminating :FAKE_PID"
assert log.message == expected assert log.message == expected
:ok :ok

View File

@ -91,6 +91,7 @@ defmodule FarmbotOS.SysCalls.MovementTest do
assert msg == error_log assert msg == error_log
end end
@tag :capture_log
test "move_absolute/4 - error (in tuple)" do test "move_absolute/4 - error (in tuple)" do
expect(FarmbotFirmware, :request, 1, fn {:parameter_read, [_]} -> expect(FarmbotFirmware, :request, 1, fn {:parameter_read, [_]} ->
{:error, "boom"} {:error, "boom"}

View File

@ -6,6 +6,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do
alias FarmbotCore.Asset.Peripheral alias FarmbotCore.Asset.Peripheral
@digital 0 @digital 0
@tag :capture_log
test "read_pin with %Peripheral{}, pin is 1" do test "read_pin with %Peripheral{}, pin is 1" do
expect(FarmbotFirmware, :request, 1, fn expect(FarmbotFirmware, :request, 1, fn
{:pin_read, [p: 13, m: 0]} -> {:pin_read, [p: 13, m: 0]} ->
@ -20,6 +21,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do
assert 1 == PinControl.read_pin(peripheral, @digital) assert 1 == PinControl.read_pin(peripheral, @digital)
end end
@tag :capture_log
test "read_pin with %Peripheral{}, pin is 0" do test "read_pin with %Peripheral{}, pin is 0" do
expect(FarmbotFirmware, :request, 1, fn expect(FarmbotFirmware, :request, 1, fn
{:pin_read, [p: 13, m: 0]} -> {:pin_read, [p: 13, m: 0]} ->
@ -30,6 +32,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do
assert 0 == PinControl.read_pin(peripheral, @digital) assert 0 == PinControl.read_pin(peripheral, @digital)
end end
@tag :capture_log
test "toggle_pin, 1 => 0" do test "toggle_pin, 1 => 0" do
expect(FarmbotCore.Asset, :get_peripheral_by_pin, 1, fn 12 -> expect(FarmbotCore.Asset, :get_peripheral_by_pin, 1, fn 12 ->
nil nil
@ -48,6 +51,7 @@ defmodule FarmbotOS.SysCalls.PinControlTest do
assert :ok = PinControl.toggle_pin(12) assert :ok = PinControl.toggle_pin(12)
end end
@tag :capture_log
test "toggle_pin, 0 => 1" do test "toggle_pin, 0 => 1" do
expect(FarmbotCore.Asset, :get_peripheral_by_pin, 1, fn 12 -> expect(FarmbotCore.Asset, :get_peripheral_by_pin, 1, fn 12 ->
nil nil
@ -71,15 +75,14 @@ defmodule FarmbotOS.SysCalls.PinControlTest do
end end
test "set_servo_angle" do test "set_servo_angle" do
expect(FarmbotFirmware, :command, 1, fn expect(FarmbotFirmware, :command, 2, fn
{:servo_write, [p: 20, v: 90]} -> {:error, "opps"} {:servo_write, [p: 20, v: 90]} -> {:error, "opps"}
{:servo_write, [p: 40, v: 180]} -> :ok {:servo_write, [p: 40, v: 180]} -> :ok
end) end)
assert :ok = PinControl.set_servo_angle(40, 180) assert :ok = PinControl.set_servo_angle(40, 180)
message = message = "Firmware error @ \"set_servo_angle\": \"opps\""
"Firmware error @ \"set_servo_angle\": \"Can't send command when in :transport_boot state\""
assert {:error, ^message} = PinControl.set_servo_angle(20, 90) assert {:error, ^message} = PinControl.set_servo_angle(20, 90)
end end

View File

@ -101,6 +101,7 @@ defmodule FarmbotOS.SysCalls.PointLookupTest do
assert pg == PointLookup.get_point_group(pg.id) assert pg == PointLookup.get_point_group(pg.id)
end end
@tag :capture_log
test "PointLookup.get_point_group/1 - string" do test "PointLookup.get_point_group/1 - string" do
Repo.delete_all(PointGroup) Repo.delete_all(PointGroup)
Repo.delete_all(Point) Repo.delete_all(Point)

View File

@ -1,20 +1,26 @@
Application.ensure_all_started(:mimic) Application.ensure_all_started(:mimic)
Mimic.copy(FarmbotCore.Asset.Device)
Mimic.copy(FarmbotCore.Asset.FbosConfig) [
Mimic.copy(FarmbotCore.Asset.FirmwareConfig) FarmbotCore.Asset.Device,
Mimic.copy(FarmbotCore.Asset) FarmbotCore.Asset.FbosConfig,
Mimic.copy(FarmbotCore.BotState) FarmbotCore.Asset.FirmwareConfig,
Mimic.copy(FarmbotCore.Config) FarmbotCore.Asset,
Mimic.copy(FarmbotCore.FarmwareRuntime) FarmbotCore.BotState,
Mimic.copy(FarmbotCore.LogExecutor) FarmbotCore.Config,
Mimic.copy(FarmbotExt.API.Reconciler) FarmbotCore.FarmwareRuntime,
Mimic.copy(FarmbotExt.API) FarmbotCore.LogExecutor,
Mimic.copy(FarmbotFirmware) FarmbotExt.API.Reconciler,
Mimic.copy(FarmbotOS.Configurator.ConfigDataLayer) FarmbotExt.API,
Mimic.copy(FarmbotOS.Configurator.DetsTelemetryLayer) FarmbotFirmware,
Mimic.copy(FarmbotOS.Configurator.FakeNetworkLayer) FarmbotOS.Configurator.ConfigDataLayer,
Mimic.copy(FarmbotOS.SysCalls.Movement) FarmbotOS.Configurator.DetsTelemetryLayer,
Mimic.copy(FarmbotOS.SysCalls) FarmbotOS.Configurator.FakeNetworkLayer,
Mimic.copy(File) FarmbotOS.SysCalls.Movement,
Mimic.copy(MuonTrap) FarmbotOS.SysCalls,
File,
MuonTrap,
FarmbotCeleryScript.SysCalls
]
|> Enum.map(&Mimic.copy/1)
ExUnit.start() ExUnit.start()

View File

@ -1,56 +0,0 @@
defmodule Mix.Tasks.Farmbot.Coveralls do
@moduledoc """
Mix Task to report the coverage for all of the individual projects that make
up the repository.
"""
use Mix.Task
Module.register_attribute(__MODULE__, :projects, accumulate: true)
@projects :farmbot_celery_script
@projects :farmbot_core
@projects :farmbot_ext
@projects :farmbot_firmware
@projects :farmbot_os
# @projects :farmbot_telemetry
def run(args) do
@projects
|> pmap(&read_coverage_json!/1)
|> List.flatten()
|> run_task(args)
end
def run_task(stats, []) do
run_task(stats, ["local"])
end
def run_task(stats, ["local"]) do
ExCoveralls.Local.execute(stats, [])
end
def run_task(stats, ["circle"]) do
ExCoveralls.Circle.execute(stats, [])
end
def pmap(data, func) do
data
|> Enum.map(&(Task.async(fn -> func.(&1) end)))
|> Enum.map(&Task.await/1)
end
def read_coverage_json!(project) do
coverage_file = Path.join([to_string(project), "cover", "excoveralls.json"])
with {:ok, bin} <- File.read(coverage_file),
{:ok, json} <- Jason.decode(bin) do
Enum.map(json["source_files"], fn(%{"name" => name, "source" => source, "coverage" => coverage}) ->
%{name: Path.join([to_string(project), name]), source: source, coverage: coverage}
end)
else
_ -> Mix.raise("""
Could not read coverage JSON from #{coverage_file}.
Make sure to run `mix coveralls.json` in each project's parent
directory.
""")
end
end
end

View File

@ -1,6 +1,4 @@
defmodule Farmbot.TestSupport.AssetFixtures do defmodule Farmbot.TestSupport.AssetFixtures do
alias FarmbotCore.Asset
alias FarmbotCore.Asset.{ alias FarmbotCore.Asset.{
Device, Device,
FarmEvent, FarmEvent,
@ -10,12 +8,12 @@ defmodule Farmbot.TestSupport.AssetFixtures do
Sequence Sequence
} }
def regimen_instance(regimen_params, farm_event_params, params \\ %{}) do # def regimen_instance(regimen_params, farm_event_params, params \\ %{}) do
regimen = regimen(regimen_params) # regimen = regimen(regimen_params)
farm_event = regimen_event(regimen, farm_event_params) # farm_event = regimen_event(regimen, farm_event_params)
params = Map.merge(%{id: :rand.uniform(10000), monitor: false}, params) # params = Map.merge(%{id: :rand.uniform(10000), monitor: false}, params)
Asset.new_regimen_instance!(farm_event, params) # Asset.new_regimen_instance!(farm_event, params)
end # end
def fbos_config(params \\ %{}) do def fbos_config(params \\ %{}) do
default = %{ default = %{
@ -77,30 +75,6 @@ defmodule Farmbot.TestSupport.AssetFixtures do
|> Repo.insert!() |> Repo.insert!()
end end
def sequence_event(sequence, params \\ %{}) do
now = DateTime.utc_now()
params =
Map.merge(
%{
id: :rand.uniform(1_000_000),
monitor: false,
executable_type: "Sequence",
executable_id: sequence.id,
start_time: now,
end_time: now,
repeat: 0,
time_unit: "never"
},
params
)
FarmEvent
|> struct()
|> FarmEvent.changeset(params)
|> Repo.insert!()
end
@doc """ @doc """
Instantiates, but does not create, a %Device{} Instantiates, but does not create, a %Device{}
""" """