Merge pull request #1148 from FarmBot/rel-9.1.0

Release v9.1.0
pull/1149/head v9.1.0
Rick Carlino 2020-02-12 20:49:46 -06:00 committed by GitHub
commit 6ed34124e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 7367 additions and 14412 deletions

View File

@ -162,7 +162,9 @@ deploy_nerves_hub_firmware_steps: &deploy_nerves_hub_firmware_steps
- run:
name: Sign Image
working_directory: /nerves/build/farmbot_os
command: mix nerves_hub.firmware sign --key notreal /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat ../VERSION).fw
command: |
mix deps.get
mix nerves_hub.firmware sign --key notreal /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat ../VERSION).fw
- run:
name: Publish to NervesHub
working_directory: /nerves/build/farmbot_os

View File

@ -1,5 +1,24 @@
# Changelog
# 9.1.0
* Improved support for new FarmBot Express models
* Various firmware bug fixes for Express models.
* Bug fix for slow Farmware execution (Thanks, @jsimmonds2)
* Dependency upgrades
* Upgrade VintageNet (networking library)
* Removal of `dump_info` RPCs
* Numerous internal improvements, such as increasing test coverage and changing dependency injection scheme.
* Fix issue where firmware commands would be tried too many times.
# 9.0.4
* Bug fix for slow Farmware execution (Thanks, @jsimmonds2)
* Dependency upgrades
* Upgrade VintageNet (networking library)
* Removal of `dump_info` RPCs
* Numerous internal improvements, such as increasing test coverage and changing dependency injection scheme.
# 9.0.3
* Dependency updates

View File

@ -2,12 +2,9 @@
# :floppy_disk: Latest OS Image Downloads
<!-- DON'T CHANGE THE TEXT ABOVE. It is used in documentation links. -->
Download the version of FarmBot OS that corresponds to the FarmBot kit and computer you have:
The FarmBot OS release page has moved to [my.farm.bot/os](https://my.farm.bot/os)
| FarmBot Kit | Computer | Download Link |
| --- | --- | --- |
| Genesis v1.2, Genesis v1.3, Genesis v1.4, Genesis XL v1.4 | Raspberry Pi 3 | [Download FBOS](https://github.com/FarmBot/farmbot_os/releases/download/v9.0.3/farmbot-rpi3-9.0.3.img) |
| Express v1.0, Express XL v1.0 | Raspberry Pi Zero W | Coming soon |
Old versions of FarmBot OS can be found [here](https://github.com/FarmBot/farmbot_os/releases). Please note that [FarmBot does not provide support for old versions of FBOS](https://software.farm.bot/docs/support-policy).
---

View File

@ -1 +1 @@
9.0.3
9.1.0

View File

@ -1,5 +1,7 @@
{
"skip_files": [
"lib/farmbot_celery_script/compiler/tools.ex"
]
],
"minimum_coverage": 53,
"treat_no_relevant_lines_as_covered": true
}

View File

@ -92,7 +92,6 @@
"calibrate",
"change_ownership",
"check_updates",
"dump_info",
"emergency_lock",
"emergency_unlock",
"execute",
@ -832,18 +831,6 @@
],
"docs": ""
},
{
"allowed_args": [],
"allowed_body_types": [],
"name": "dump_info",
"tags": [
"function",
"network_user",
"disk_user",
"api_writer"
],
"docs": "Sends an info dump to server administrators for troubleshooting."
},
{
"allowed_args": [],
"allowed_body_types": [],
@ -1170,7 +1157,6 @@
"calibrate",
"change_ownership",
"check_updates",
"dump_info",
"emergency_lock",
"emergency_unlock",
"execute",
@ -1242,7 +1228,6 @@
"calibrate",
"change_ownership",
"check_updates",
"dump_info",
"emergency_lock",
"emergency_unlock",
"execute",

View File

@ -31,11 +31,6 @@ defmodule FarmbotCeleryScript.AST.Factory do
)
end
def dump_info(%AST{} = ast) do
ast
|> add_body_node(new(:dump_info))
end
def emergency_lock(%AST{} = ast) do
ast
|> add_body_node(new(:emergency_lock))

View File

@ -279,12 +279,6 @@ defmodule FarmbotCeleryScript.Compiler do
end
end
def dump_info(_, _env) do
quote location: :keep do
FarmbotCeleryScript.SysCalls.dump_info()
end
end
defp print_compiled_code(compiled) do
IO.puts("========")

View File

@ -29,7 +29,6 @@ defmodule FarmbotCeleryScript.SysCalls do
@callback check_update() :: ok_or_error
@callback coordinate(x :: number, y :: number, z :: number) ::
%{x: number(), y: number(), z: number()} | error
@callback dump_info() :: ok_or_error
@callback emergency_lock() :: ok_or_error
@callback emergency_unlock() :: ok_or_error
@callback execute_script(package, args :: map()) :: ok_or_error
@ -172,10 +171,6 @@ defmodule FarmbotCeleryScript.SysCalls do
coord_or_error(sys_calls, :coordinate, [x, y, z])
end
def dump_info(sys_calls \\ @sys_calls) do
ok_or_error(sys_calls, :dump_info, [])
end
def emergency_lock(sys_calls \\ @sys_calls) do
ok_or_error(sys_calls, :emergency_lock, [])
end

View File

@ -28,9 +28,6 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
@impl true
def coordinate(x, y, z), do: error(:coordinate, [x, y, z])
@impl true
def dump_info(), do: error(:dump_info, [])
@impl true
def emergency_lock(), do: error(:emergency_lock, [])

View File

@ -3,7 +3,10 @@ defmodule FarmbotCeleryScript.Corpus.NodeTest do
alias FarmbotCeleryScript.Corpus
test "inspect" do
assert "Sequence(version, locals) [calibrate, change_ownership, check_updates, dump_info, emergency_lock, emergency_unlock, execute, execute_script, factory_reset, find_home, flash_firmware, home, install_farmware, install_first_party_farmware, _if, move_absolute, move_relative, power_off, read_pin, read_status, reboot, remove_farmware, resource_update, send_message, set_servo_angle, set_user_env, sync, take_photo, toggle_pin, update_farmware, wait, write_pin, zero]" =
inspect(Corpus.sequence())
a =
"Sequence(version, locals) [calibrate, change_ownership, check_updates, emergency_lock, emergency_unlock, execute, execute_script, factory_reset, find_home, flash_firmware, home, install_farmware, install_first_party_farmware, _if, move_absolute, move_relative, power_off, read_pin, read_status, reboot, remove_farmware, resource_update, send_message, set_servo_angle, set_user_env, sync, take_photo, toggle_pin, update_farmware, wait, write_pin, zero]"
b = inspect(Corpus.sequence())
assert a == b
end
end

View File

@ -35,7 +35,6 @@ config :farmbot_core, FarmbotCore.BotState.FileSystem,
sleep_time: 200
config :farmbot_core, FarmbotCore.EctoMigrator,
expected_fw_versions: ["6.4.2.F", "6.4.2.R", "6.4.2.G"],
default_firmware_io_logs: false,
default_server: "https://my.farm.bot",
default_dns_name: "my.farm.bot",

View File

@ -9,7 +9,6 @@ defmodule FarmbotCore.Asset do
Repo,
Device,
DeviceCert,
DiagnosticDump,
FarmwareEnv,
FirstPartyFarmware,
FarmwareInstallation,
@ -54,6 +53,7 @@ defmodule FarmbotCore.Asset do
if device = Repo.get_by(Device, id: id) do
Repo.delete!(device)
end
:ok
end
@ -78,13 +78,14 @@ defmodule FarmbotCore.Asset do
end
def update_farm_event!(farm_event, params) do
farm_event =
farm_event |>
FarmEvent.changeset(params)
farm_event =
farm_event
|> FarmEvent.changeset(params)
|> Repo.update!()
if farm_event.executable_type == "Regimen" do
regimen_instance = get_regimen_instance(farm_event)
if regimen_instance do
regimen_instance
|> Repo.preload([:farm_event, :regimen])
@ -111,9 +112,11 @@ defmodule FarmbotCore.Asset do
def get_farm_event_execution(%FarmEvent{} = farm_event, scheduled_at) do
Repo.one(
from e in FarmEvent.Execution,
where: e.farm_event_local_id == ^farm_event.local_id
and e.scheduled_at == ^scheduled_at
from(e in FarmEvent.Execution,
where:
e.farm_event_local_id == ^farm_event.local_id and
e.scheduled_at == ^scheduled_at
)
)
end
@ -149,6 +152,7 @@ defmodule FarmbotCore.Asset do
if fbos_config = Repo.get_by(FbosConfig, id: id) do
Repo.delete!(fbos_config)
end
:ok
end
@ -177,6 +181,7 @@ defmodule FarmbotCore.Asset do
if firmware_config = Repo.get_by(FirmwareConfig, id: id) do
Repo.delete!(firmware_config)
end
:ok
end
@ -192,12 +197,19 @@ defmodule FarmbotCore.Asset do
end
def get_regimen_instance(%FarmEvent{} = farm_event) do
regimen = Repo.one(from r in Regimen, where: r.id == ^farm_event.executable_id)
regimen && Repo.one(from ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id)
regimen = Repo.one(from(r in Regimen, where: r.id == ^farm_event.executable_id))
regimen &&
Repo.one(
from(ri in RegimenInstance,
where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id
)
)
end
def new_regimen_instance!(%FarmEvent{} = farm_event, params \\ %{}) do
regimen = Repo.one!(from r in Regimen, where: r.id == ^farm_event.executable_id)
regimen = Repo.one!(from(r in Regimen, where: r.id == ^farm_event.executable_id))
RegimenInstance.changeset(%RegimenInstance{}, params)
|> Ecto.Changeset.put_assoc(:regimen, regimen)
|> Ecto.Changeset.put_assoc(:farm_event, farm_event)
@ -207,7 +219,7 @@ defmodule FarmbotCore.Asset do
def delete_regimen_instance!(%RegimenInstance{} = ri) do
Repo.delete!(ri)
end
def add_execution_to_regimen_instance!(%RegimenInstance{} = ri, params \\ %{}) do
%RegimenInstance.Execution{}
|> RegimenInstance.Execution.changeset(params)
@ -217,9 +229,11 @@ defmodule FarmbotCore.Asset do
def get_regimen_instance_execution(%RegimenInstance{} = ri, scheduled_at) do
Repo.one(
from e in RegimenInstance.Execution,
where: e.regimen_instance_local_id == ^ri.local_id
and e.scheduled_at == ^scheduled_at
from(e in RegimenInstance.Execution,
where:
e.regimen_instance_local_id == ^ri.local_id and
e.scheduled_at == ^scheduled_at
)
)
end
@ -248,7 +262,7 @@ defmodule FarmbotCore.Asset do
@doc "Returns all points matching Point.pointer_type"
def get_all_points_by_type(type, order_by \\ "random") do
(from p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at))
from(p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at))
|> Repo.all()
|> sort_points(order_by)
end
@ -257,7 +271,7 @@ defmodule FarmbotCore.Asset do
points
|> Enum.group_by(&group_points_by(&1, order_by))
|> Enum.sort(&group_sort(&1, &2, order_by))
|> Enum.map(fn({_group_index, group}) -> Enum.sort(group, &sort_points(&1, &2, order_by)) end)
|> Enum.map(fn {_group_index, group} -> Enum.sort(group, &sort_points(&1, &2, order_by)) end)
|> List.flatten()
end
@ -272,7 +286,6 @@ defmodule FarmbotCore.Asset do
def group_sort({lgroup, _}, {rgroup, _}, "yx_descending"), do: lgroup >= rgroup
def group_sort(_, _, "random"), do: Enum.random([true, false])
def sort_points(%{y: ly}, %{y: ry}, "xy_ascending"), do: ly <= ry
def sort_points(%{y: ly}, %{y: ry}, "xy_descending"), do: ly >= ry
def sort_points(%{x: lx}, %{x: rx}, "yx_ascending"), do: lx <= rx
@ -285,15 +298,15 @@ defmodule FarmbotCore.Asset do
def get_point_group(params) do
case Repo.get_by(PointGroup, params) do
nil ->
nil ->
nil
%{sort_type: nil} = group ->
group
%{sort_type: nil} = group ->
group
%{point_ids: unsorted, sort_type: sort_by} = point_group ->
sorted =
Repo.all(from p in Point, where: p.id in ^unsorted)
sorted =
Repo.all(from(p in Point, where: p.id in ^unsorted))
|> sort_points(sort_by)
|> Enum.map(&Map.fetch!(&1, :id))
@ -308,7 +321,7 @@ defmodule FarmbotCore.Asset do
end
def update_point_group!(point_group, params) do
updated =
updated =
point_group
|> PointGroup.changeset(params)
|> Repo.update!()
@ -322,11 +335,11 @@ defmodule FarmbotCore.Asset do
for asset <- farm_events ++ regimen_instances do
# TODO(Connor) this might be worth creating a behaviour for
if uses_point_group?(asset, point_group) do
Logger.debug "#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it."
Logger.debug("#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it.")
FarmbotCore.AssetSupervisor.update_child(asset)
end
end
updated
end
@ -335,14 +348,16 @@ defmodule FarmbotCore.Asset do
end
def uses_point_group?(%FarmEvent{body: body}, %PointGroup{id: point_group_id}) do
any_body_node_uses_point_group?(body, point_group_id)
end
def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{id: point_group_id}) do
any_body_node_uses_point_group?(body, point_group_id) || Enum.find(regimen_items, fn(%{sequence_id: sequence_id}) ->
any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id)
end)
def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{
id: point_group_id
}) do
any_body_node_uses_point_group?(body, point_group_id) ||
Enum.find(regimen_items, fn %{sequence_id: sequence_id} ->
any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id)
end)
end
def uses_point_group?(%RegimenInstance{farm_event: farm_event, regimen: regimen}, point_group) do
@ -350,11 +365,13 @@ defmodule FarmbotCore.Asset do
end
def any_body_node_uses_point_group?(body, point_group_id) do
Enum.find(body, fn
Enum.find(body, fn
%{
kind: "execute",
body: execute_body
} -> any_body_node_uses_point_group?(execute_body, point_group_id)
} ->
any_body_node_uses_point_group?(execute_body, point_group_id)
%{
args: %{
"data_value" => %{
@ -364,7 +381,8 @@ defmodule FarmbotCore.Asset do
"label" => "parent"
},
kind: "parameter_application"
} -> true
} ->
true
%{
args: %{
@ -375,8 +393,11 @@ defmodule FarmbotCore.Asset do
"label" => "parent"
},
kind: "parameter_application"
} -> true
_ -> false
} ->
true
_ ->
false
end)
end
@ -407,6 +428,7 @@ defmodule FarmbotCore.Asset do
def new_public_key_from_home!() do
public_key_path = Path.join([System.get_env("HOME"), ".ssh", "id_rsa.pub"])
public_key = File.read!(public_key_path)
%PublicKey{}
|> PublicKey.changeset(%{public_key: public_key})
|> Repo.insert()
@ -417,7 +439,7 @@ defmodule FarmbotCore.Asset do
|> PublicKey.changeset(%{public_key: public_key})
|> Repo.insert()
end
## End PublicKey
## Begin Regimen
@ -435,18 +457,23 @@ defmodule FarmbotCore.Asset do
end
def delete_regimen!(regimen) do
regimen_instances = Repo.all(from ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id)
regimen_instances =
Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))
for ri <- regimen_instances do
IO.puts "deleting regimen instance: #{inspect(ri)}"
IO.puts("deleting regimen instance: #{inspect(ri)}")
delete_regimen_instance!(ri)
end
Repo.delete!(regimen)
end
@doc "Update an existing regimen"
def update_regimen!(regimen, params) do
regimen_instances = Repo.all(from ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id)
|> Repo.preload([:farm_event, :regimen])
regimen_instances =
Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))
|> Repo.preload([:farm_event, :regimen])
for ri <- regimen_instances do
ri
|> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()})
@ -469,23 +496,31 @@ defmodule FarmbotCore.Asset do
def update_sequence!(%Sequence{} = sequence, params \\ %{}) do
sequence_id = sequence.id
farm_events = Repo.all(from f in FarmEvent,
where: f.executable_type == "Sequence"
and f.executable_id == ^sequence_id)
regimen_instances = RegimenInstance
|> Repo.all()
|> Repo.preload([:regimen, :farm_event])
|> Enum.filter(fn
%{regimen: %{regimen_items: items}} ->
Enum.find(items, fn
%{sequence_id: ^sequence_id} -> true
%{sequence_id: _} -> true
farm_events =
Repo.all(
from(f in FarmEvent,
where:
f.executable_type == "Sequence" and
f.executable_id == ^sequence_id
)
)
regimen_instances =
RegimenInstance
|> Repo.all()
|> Repo.preload([:regimen, :farm_event])
|> Enum.filter(fn
%{regimen: %{regimen_items: items}} ->
Enum.find(items, fn
%{sequence_id: ^sequence_id} -> true
%{sequence_id: _} -> true
end)
%{regimen: nil} ->
false
end)
%{regimen: nil} -> false
end)
for asset <- farm_events ++ regimen_instances do
FarmbotCore.AssetSupervisor.update_child(asset)
end
@ -507,10 +542,11 @@ defmodule FarmbotCore.Asset do
def get_farmware_manifest(package) do
first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware, select: fwi.manifest))
regular_farmwares = Repo.all(from(fwi in FarmwareInstallation, select: fwi.manifest))
Enum.find(
first_party_farmwares ++ regular_farmwares,
fn
%{package: pkg} -> pkg == package
first_party_farmwares ++ regular_farmwares,
fn
%{package: pkg} -> pkg == package
_ -> false
end
)
@ -519,10 +555,11 @@ defmodule FarmbotCore.Asset do
def get_farmware_installation(package) do
first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware))
regular_farmwares = Repo.all(from(fwi in FarmwareInstallation))
Enum.find(
first_party_farmwares ++ regular_farmwares,
fn
%{manifest: %{package: pkg}} -> pkg == package
first_party_farmwares ++ regular_farmwares,
fn
%{manifest: %{package: pkg}} -> pkg == package
_ -> false
end
)
@ -530,12 +567,14 @@ defmodule FarmbotCore.Asset do
def upsert_farmware_manifest_by_id(id, params) do
fwi = Repo.get_by(FarmwareInstallation, id: id) || %FarmwareInstallation{}
FarmwareInstallation.changeset(fwi, params)
|> Repo.insert_or_update()
end
def upsert_first_party_farmware_manifest_by_id(id, params) do
fwi = Repo.get_by(FirstPartyFarmware, id: id) || %FirstPartyFarmware{}
FirstPartyFarmware.changeset(fwi, params)
|> Repo.insert_or_update()
end
@ -557,12 +596,14 @@ defmodule FarmbotCore.Asset do
def new_farmware_env(params) do
key = params["key"] || params[:key]
fwe = with key when is_binary(key) <- key,
[fwe | _] <- Repo.all(from fwe in FarmwareEnv, where: fwe.key == ^key) do
fwe
else
_ -> %FarmwareEnv{}
end
fwe =
with key when is_binary(key) <- key,
[fwe | _] <- Repo.all(from(fwe in FarmwareEnv, where: fwe.key == ^key)) do
fwe
else
_ -> %FarmwareEnv{}
end
FarmwareEnv.changeset(fwe, params)
|> Repo.insert_or_update()
@ -596,7 +637,7 @@ defmodule FarmbotCore.Asset do
Sensor.changeset(%Sensor{}, params)
|> Repo.insert!()
end
def update_sensor!(sensor, params) do
sensor
|> Sensor.changeset(params)
@ -624,15 +665,6 @@ defmodule FarmbotCore.Asset do
## End SensorReading
## Begin DiagnosticDump
def new_diagnostic_dump(params) do
DiagnosticDump.changeset(%DiagnosticDump{}, params)
|> Repo.insert()
end
## End DiagnosticDump
## Begin DeviceCert
def new_device_cert(params) do

View File

@ -4,6 +4,7 @@ defmodule FarmbotCore.Asset.Command do
"""
require Logger
alias FarmbotCore.{Asset, Asset.Repo}
alias FarmbotCore.Asset.{
Device,
FarmEvent,
@ -44,31 +45,31 @@ defmodule FarmbotCore.Asset.Command do
:ok
end
def update(Device, _id, params) do
def update(Device, _id, params) do
Asset.update_device!(params)
:ok
end
def update(FbosConfig, id, nil) do
def update(FbosConfig, id, nil) do
Asset.delete_fbos_config!(id)
:ok
end
def update(FbosConfig, _id, params) do
def update(FbosConfig, _id, params) do
Asset.update_fbos_config!(params)
:ok
end
def update(FirmwareConfig, id, nil) do
def update(FirmwareConfig, id, nil) do
Asset.delete_firmware_config!(id)
:ok
end
def update(FirmwareConfig, _id, params) do
def update(FirmwareConfig, _id, params) do
Asset.update_firmware_config!(params)
:ok
end
# Deletion use case:
# TODO(Connor) put checks for deleting Device, FbosConfig and FirmwareConfig
@ -96,12 +97,12 @@ defmodule FarmbotCore.Asset.Command do
:ok
end
def update(FarmwareEnv, id, params) do
def update(FarmwareEnv, id, params) do
Asset.upsert_farmware_env_by_id(id, params)
:ok
end
def update(FarmwareInstallation, id, params) do
def update(FarmwareInstallation, id, params) do
Asset.upsert_farmware_manifest_by_id(id, params)
:ok
end
@ -112,15 +113,17 @@ defmodule FarmbotCore.Asset.Command do
def update(FarmEvent, id, params) do
old = Asset.get_farm_event(id)
if old,
do: Asset.update_farm_event!(old, params),
if old,
do: Asset.update_farm_event!(old, params),
else: Asset.new_farm_event!(params)
:ok
end
def update(PublicKey, id, params) do
old = Asset.get_public_key(id)
if old,
do: Asset.update_public_key!(old, params),
else: Asset.new_public_key!(params)
@ -130,33 +133,37 @@ defmodule FarmbotCore.Asset.Command do
def update(Regimen, id, params) do
old = Asset.get_regimen(id)
if old,
do: Asset.update_regimen!(old, params),
if old,
do: Asset.update_regimen!(old, params),
else: Asset.new_regimen!(params)
:ok
end
def update(Sensor, id, params) do
old = Asset.get_sensor(id)
if old,
do: Asset.update_sensor!(old, params),
if old,
do: Asset.update_sensor!(old, params),
else: Asset.new_sensor!(params)
:ok
end
def update(SensorReading, id, params) do
old = Asset.get_sensor_reading(id)
if old,
do: Asset.update_sensor_reading!(old, params),
if old,
do: Asset.update_sensor_reading!(old, params),
else: Asset.new_sensor_reading!(params)
:ok
end
def update(Sequence, id, params) do
old = Asset.get_sequence(id)
if old,
do: Asset.update_sequence!(old, params),
else: Asset.new_sequence!(params)
@ -172,6 +179,7 @@ defmodule FarmbotCore.Asset.Command do
def update(PointGroup, id, params) do
old = Asset.get_point_group(id: id)
if old,
do: Asset.update_point_group!(old, params),
else: Asset.new_point_group!(params)
@ -181,8 +189,9 @@ defmodule FarmbotCore.Asset.Command do
# Catch-all use case:
def update(asset_kind, id, params) do
Logger.warn "AssetCommand needs implementation: #{asset_kind}"
Logger.warn("AssetCommand needs implementation: #{asset_kind}")
mod = as_module!(asset_kind)
case Repo.get_by(mod, id: id) do
nil ->
struct!(mod)
@ -205,28 +214,27 @@ defmodule FarmbotCore.Asset.Command do
mod.changeset(asset, params)
end
defp as_module!("Device"), do: Asset.Device
defp as_module!("DiagnosticDump"), do: Asset.DiagnosticDump
defp as_module!("FarmEvent"), do: Asset.FarmEvent
defp as_module!("FarmwareEnv"), do: Asset.FarmwareEnv
defp as_module!("FirstPartyFarmware"), do: Asset.FirstPartyFarmware
defp as_module!("FarmwareInstallation"), do: Asset.FarmwareInstallation
defp as_module!("FbosConfig"), do: Asset.FbosConfig
defp as_module!("FirmwareConfig"), do: Asset.FirmwareConfig
defp as_module!("Peripheral"), do: Asset.Peripheral
defp as_module!("PinBinding"), do: Asset.PinBinding
defp as_module!("Point"), do: Asset.Point
defp as_module!("PointGroup"), do: Asset.PointGroup
defp as_module!("Regimen"), do: Asset.Regimen
defp as_module!("Sensor"), do: Asset.Sensor
defp as_module!("SensorReading"), do: Asset.SensorReading
defp as_module!("Sequence"), do: Asset.Sequence
defp as_module!("Tool"), do: Asset.Tool
defp as_module!("Device"), do: Asset.Device
defp as_module!("FarmEvent"), do: Asset.FarmEvent
defp as_module!("FarmwareEnv"), do: Asset.FarmwareEnv
defp as_module!("FirstPartyFarmware"), do: Asset.FirstPartyFarmware
defp as_module!("FarmwareInstallation"), do: Asset.FarmwareInstallation
defp as_module!("FbosConfig"), do: Asset.FbosConfig
defp as_module!("FirmwareConfig"), do: Asset.FirmwareConfig
defp as_module!("Peripheral"), do: Asset.Peripheral
defp as_module!("PinBinding"), do: Asset.PinBinding
defp as_module!("Point"), do: Asset.Point
defp as_module!("PointGroup"), do: Asset.PointGroup
defp as_module!("Regimen"), do: Asset.Regimen
defp as_module!("Sensor"), do: Asset.Sensor
defp as_module!("SensorReading"), do: Asset.SensorReading
defp as_module!("Sequence"), do: Asset.Sequence
defp as_module!("Tool"), do: Asset.Tool
defp as_module!(module) when is_atom(module) do
as_module!(List.last(Module.split(module)))
end
defp as_module!(kind) when is_binary(kind) do
raise("""
Unknown kind: #{kind}

View File

@ -1,58 +0,0 @@
defmodule FarmbotCore.Asset.DiagnosticDump do
@moduledoc """
Just the DiagDump REST resource, used by FarmBot staff to help users debug
remote device problems.
"""
use FarmbotCore.Asset.Schema, path: "/api/diagnostic_dumps"
schema "diagnostic_dumps" do
field(:id, :id)
has_one(:local_meta, FarmbotCore.Asset.Private.LocalMeta,
on_delete: :delete_all,
references: :local_id,
foreign_key: :asset_local_id
)
field(:ticket_identifier, :string)
field(:fbos_commit, :string)
field(:fbos_version, :string)
field(:firmware_commit, :string)
field(:firmware_state, :string)
field(:network_interface, :string)
field(:fbos_dmesg_dump, :string)
field(:monitor, :boolean, default: true)
timestamps()
end
view diagnostic_dump do
%{
id: diagnostic_dump.id,
ticket_identifier: diagnostic_dump.ticket_identifier,
fbos_commit: diagnostic_dump.fbos_commit,
fbos_version: diagnostic_dump.fbos_version,
firmware_commit: diagnostic_dump.firmware_commit,
firmware_state: diagnostic_dump.firmware_state,
network_interface: diagnostic_dump.network_interface,
fbos_dmesg_dump: diagnostic_dump.fbos_dmesg_dump
}
end
def changeset(diagnostic_dump, params \\ %{}) do
diagnostic_dump
|> cast(params, [
:id,
:ticket_identifier,
:fbos_commit,
:fbos_version,
:firmware_commit,
:firmware_state,
:network_interface,
:fbos_dmesg_dump,
:monitor,
:created_at,
:updated_at
])
|> validate_required([])
end
end

View File

@ -100,16 +100,15 @@ defimpl String.Chars, for: FarmbotCore.Asset.PinBinding do
end
defp special_action(button_number, action, pin_num) do
"Button #{button_number}: #{format_action(action)} (Pi #{pin_num})"
"Button #{button_number}: #{format_action(action)} (Pi #{pin_num})"
end
defp format_action("dump_info"), do: "Dump Info"
defp format_action("emergency_lock"), do: "E-Stop"
defp format_action("emergency_lock"), do: "E-Stop"
defp format_action("emergency_unlock"), do: "E-Unlock"
defp format_action("power_off"), do: "Power Off"
defp format_action("read_status"), do: "Read Status"
defp format_action("reboot"), do: "Reboot"
defp format_action("sync"), do: "Sync"
defp format_action("take_photo"), do: "Take Photo"
defp format_action("take_photo"), do: "Take Photo"
defp format_action(_), do: nil
end

View File

@ -9,7 +9,6 @@ defmodule FarmbotCore.Asset.Private.LocalMeta do
alias FarmbotCore.Asset.{
Repo,
Device,
DiagnosticDump,
DeviceCert,
FarmEvent,
FarmwareEnv,
@ -47,13 +46,6 @@ defmodule FarmbotCore.Asset.Private.LocalMeta do
define_field: false
)
belongs_to(:diagnostic_dump, DiagnosticDump,
foreign_key: :asset_local_id,
type: :binary_id,
references: :local_id,
define_field: false
)
belongs_to(:farm_event, FarmEvent,
foreign_key: :asset_local_id,
type: :binary_id,
@ -174,8 +166,7 @@ defmodule FarmbotCore.Asset.Private.LocalMeta do
"firmware_configs",
"fbos_configs",
"farmware_installations",
"farmware_envs",
"diagnostic_dumps"
"farmware_envs"
])
|> unsafe_validate_unique([:table, :asset_local_id], Repo,
message: "LocalMeta already exists."

View File

@ -41,7 +41,6 @@ defmodule FarmbotCore.Asset.Sync do
embeds_many(:devices, Item)
embeds_many(:firmware_configs, Item)
embeds_many(:fbos_configs, Item)
embeds_many(:diagnostic_dumps, Item)
embeds_many(:farm_events, Item)
embeds_many(:farmware_envs, Item)
embeds_many(:first_party_farmwares, Item)
@ -65,7 +64,6 @@ defmodule FarmbotCore.Asset.Sync do
devices: Enum.map(sync.device, &Item.render/1),
fbos_configs: Enum.map(sync.fbos_config, &Item.render/1),
firmware_configs: Enum.map(sync.firmware_config, &Item.render/1),
diagnostic_dumps: Enum.map(sync.diagnostic_dumps, &Item.render/1),
farm_events: Enum.map(sync.farm_events, &Item.render/1),
farmware_envs: Enum.map(sync.farmware_envs, &Item.render/1),
first_party_farmwares: Enum.map(sync.first_party_farmwares, &Item.render/1),
@ -90,7 +88,6 @@ defmodule FarmbotCore.Asset.Sync do
|> cast_embed(:devices)
|> cast_embed(:fbos_configs)
|> cast_embed(:firmware_configs)
|> cast_embed(:diagnostic_dumps)
|> cast_embed(:farm_events)
|> cast_embed(:farmware_envs)
|> cast_embed(:farmware_installations)

View File

@ -1,25 +1,26 @@
defmodule FarmbotCore.AssetHelpers do
@moduledoc """
Helpers for the console at runtime.
Example:
iex()> use FarmbotCore.AssetHelpers
iex()> Repo.all(Device)
[%Device{}]
"""
@doc false
defmacro __using__(_opts) do
require Logger
Logger.warn "Don't use this in production please!"
Logger.warn("Don't use this in production please!")
quote do
import Ecto.Query
alias FarmbotCore.Asset
alias Asset.{
Repo,
Device,
DeviceCert,
DiagnosticDump,
FarmwareEnv,
FarmwareInstallation,
FirstPartyFarmware,
@ -40,4 +41,4 @@ defmodule FarmbotCore.AssetHelpers do
}
end
end
end
end

View File

@ -33,7 +33,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Peripheral do
{:noreply, state}
end
def handle_info(:timeout, %{fw_version: "8.0.0.S"} = state) do
def handle_info(:timeout, %{fw_version: "8.0.0.S.stub"} = state) do
{:noreply, state}
end
@ -47,15 +47,15 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Peripheral do
Logger.debug("Read peripheral: #{peripheral.label}")
rpc = peripheral_to_rpc(peripheral)
case FarmbotCeleryScript.execute(rpc, make_ref()) do
:ok ->
:ok ->
Logger.debug("Read peripheral: #{peripheral.label} ok")
{:noreply, state}
{:error, reason} when errors < 5 ->
{:error, reason} when errors < 5 ->
Logger.error("Read peripheral: #{peripheral.label} error: #{reason} errors=#{state.errors}")
Process.send_after(self(), :timeout, @retry_ms)
{:noreply, %{state | errors: state.errors + 1}}
{:error, reason} when errors == 5 ->
Logger.error("Read peripheral: #{peripheral.label} error: #{reason} errors=5 not trying again.")
{:noreply, state}

View File

@ -3,12 +3,12 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
Worker for monitoring hardware GPIO. (not related to the mcu firmware.)
Upon a button trigger, a `sequence`, or `special_action` will be executed by
the CeleryScript Runtime.
the CeleryScript Runtime.
This module also defines a behaviour that allows for abstracting and testing
This module also defines a behaviour that allows for abstracting and testing
independent of GPIO hardware code.
"""
use GenServer
require Logger
require FarmbotCore.Logger
@ -75,6 +75,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
case Asset.get_sequence(pin_binding.sequence_id) do
%Sequence{name: name} = seq ->
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing #{name}")
AST.decode(seq)
|> execute(state)
@ -84,24 +85,24 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
end
end
def handle_cast(:trigger, %{pin_binding: %{special_action: "dump_info"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Dump Info")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.dump_info()
|> execute(state)
end
def handle_cast(:trigger, %{pin_binding: %{special_action: "emergency_lock"} = pin_binding} = state) do
def handle_cast(
:trigger,
%{pin_binding: %{special_action: "emergency_lock"} = pin_binding} = state
) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Lock")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.emergency_lock()
|> execute(state)
end
end
def handle_cast(:trigger, %{pin_binding: %{special_action: "emergency_unlock"} = pin_binding} = state) do
def handle_cast(
:trigger,
%{pin_binding: %{special_action: "emergency_unlock"} = pin_binding} = state
) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Unlock")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.emergency_unlock()
@ -110,14 +111,19 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "power_off"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Power Off")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.power_off()
|> execute(state)
end
def handle_cast(:trigger, %{pin_binding: %{special_action: "read_status"} = pin_binding} = state) do
def handle_cast(
:trigger,
%{pin_binding: %{special_action: "read_status"} = pin_binding} = state
) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Read Status")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.read_status()
@ -126,6 +132,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "reboot"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Reboot")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.reboot()
@ -134,6 +141,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "sync"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Sync")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.sync()
@ -142,6 +150,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "take_photo"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Take Photo")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.take_photo()
@ -182,10 +191,13 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
defp execute(%AST{} = ast, state) do
case FarmbotCeleryScript.execute(ast, make_ref()) do
:ok -> :ok
:ok ->
:ok
{:error, reason} ->
FarmbotCore.Logger.error 1, "error executing #{state.pin_binding}: #{reason}"
FarmbotCore.Logger.error(1, "error executing #{state.pin_binding}: #{reason}")
end
{:noreply, state}
end

View File

@ -3,7 +3,6 @@ defmodule FarmbotCore.FarmwareRuntime do
Handles execution of Farmware plugins.
"""
alias Avrdude.MuonTrapAdapter
alias FarmbotCeleryScript.AST
alias FarmbotCore.Asset.FarmwareInstallation.Manifest
alias FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmwareInstallation
@ -113,8 +112,7 @@ defmodule FarmbotCore.FarmwareRuntime do
# Start the plugin.
Logger.debug("spawning farmware: #{exec} #{manifest.args}")
{cmd, _} =
spawn_monitor(MuonTrapAdapter, :cmd, ["sh", ["-c", "#{exec} #{manifest.args}"], opts])
{cmd, _} = spawn_monitor(MuonTrap, :cmd, ["sh", ["-c", "#{exec} #{manifest.args}"], opts])
state = %State{
caller: caller,
@ -156,8 +154,7 @@ defmodule FarmbotCore.FarmwareRuntime do
end
def handle_info({:step_complete, ref, :ok}, %{scheduler_ref: ref} = state) do
label = UUID.uuid4()
result = %AST{kind: :rpc_ok, args: %{label: label}, body: []}
result = %AST{kind: :rpc_ok, args: %{label: state.rpc.args.label}, body: []}
ipc = add_header(result)
_reply = PipeWorker.write(state.response_pipe_handle, ipc)

View File

@ -82,31 +82,50 @@ defmodule FarmbotCore.FirmwareSideEffects do
[_, _, _, "R"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("arduino")
[_, _, _, "R", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("arduino")
# Farmduino
[_, _, _, "F"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino")
[_, _, _, "F", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino")
# Farmduino V14
[_, _, _, "G"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k14")
[_, _, _, "G", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k14")
# Farmduino V15
[_, _, _, "H"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k15")
[_, _, _, "H", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k15")
# Express V10
[_, _, _, "E"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("express_k10")
[_, _, _, "E", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("express_k10")
[_, _, _, "S"] ->
_ = Leds.red(:slow_blink)
:ok = BotState.set_firmware_version("none")
:ok = BotState.set_firmware_hardware("none")
[_, _, _, "S", _] ->
_ = Leds.red(:slow_blink)
:ok = BotState.set_firmware_version("none")
:ok = BotState.set_firmware_hardware("none")
end
end

View File

@ -0,0 +1,29 @@
# This module could have existed within FarmbotCore.Logger.
# Pulling this function into a different module facilitates
# mocking of tests.
defmodule FarmbotCore.LogExecutor do
alias FarmbotCore.Log
def execute(%Log{} = log) do
logger_meta = [
application: :farmbot,
function: log.function,
file: log.file,
line: log.line,
module: log.module,
channels: log.meta[:channels] || log.meta["channels"],
verbosity: log.verbosity,
assertion_passed: log.meta[:assertion_passed]
]
level = log.level
logger_level =
if level in [:info, :debug, :warn, :error],
do: level,
else: :info
Elixir.Logger.bare_log(logger_level, log, logger_meta)
log
end
end

View File

@ -5,6 +5,7 @@ defmodule FarmbotCore.Logger do
alias FarmbotCore.{Log, Logger.Repo}
import Ecto.Query
@log_types [:info, :debug, :busy, :warn, :success, :error, :fun, :assertion]
@doc "Send a debug message to log endpoints"
defmacro debug(verbosity, message, meta \\ []) do
@ -57,23 +58,27 @@ defmodule FarmbotCore.Logger do
def insert_log!(params) do
changeset = Log.changeset(%Log{}, params)
try do
hash = Ecto.Changeset.get_field(changeset, :hash)
case Repo.get_by(Log, hash: hash) do
nil ->
nil ->
Repo.insert!(changeset)
old ->
params =
old ->
params =
params
|> Map.put(:inserted_at, DateTime.utc_now)
|> Map.put(:inserted_at, DateTime.utc_now())
|> Map.put(:duplicates, old.duplicates + 1)
old
|> Log.changeset(params)
|> Repo.update!()
old
|> Log.changeset(params)
|> Repo.update!()
end
catch
kind, err ->
IO.warn("Error inserting log: #{kind} #{inspect(err)}", __STACKTRACE__)
IO.warn("Error inserting log: #{kind} #{inspect(err)}", __STACKTRACE__)
Ecto.Changeset.apply_changes(changeset)
end
end
@ -94,13 +99,16 @@ defmodule FarmbotCore.Logger do
@doc false
def dispatch_log(%Macro.Env{} = env, level, verbosity, message, meta)
when level in [:info, :debug, :busy, :warn, :success, :error, :fun, :assertion] and is_number(verbosity) and
is_binary(message) and is_list(meta) do
when level in @log_types and
is_number(verbosity) and
is_binary(message) and
is_list(meta) do
fun =
case env.function do
{fun, ar} -> "#{fun}/#{ar}"
nil -> "no_function"
end
%{
level: level,
verbosity: verbosity,
@ -116,29 +124,8 @@ defmodule FarmbotCore.Logger do
@doc false
def dispatch_log(params) do
params
|> insert_log!()
|> elixir_log()
end
defp elixir_log(%Log{} = log) do
logger_meta = [
application: :farmbot,
function: log.function,
file: log.file,
line: log.line,
module: log.module,
channels: log.meta[:channels] || log.meta["channels"],
verbosity: log.verbosity,
assertion_passed: log.meta[:assertion_passed]
# TODO Connor - fix time
# time: time
]
level = log.level
logger_level = if level in [:info, :debug, :warn, :error], do: level, else: :info
Elixir.Logger.bare_log(logger_level, log, logger_meta)
log
log = insert_log!(params)
FarmbotCore.LogExecutor.execute(log)
end
@doc "Helper function for deciding if a message should be logged or not."

View File

@ -1,7 +1,7 @@
defmodule FarmbotExt.API.DirtyWorker.Supervisor do
@moduledoc """
Responsible for supervising assets that will need to be
uploaded to the API via a `POST` or `PUT` request.
Responsible for supervising assets that will need to be
uploaded to the API via a `POST` or `PUT` request.
"""
use Supervisor
@ -10,7 +10,6 @@ defmodule FarmbotExt.API.DirtyWorker.Supervisor do
alias FarmbotCore.Asset.{
Device,
DeviceCert,
DiagnosticDump,
FarmEvent,
FarmwareEnv,
FarmwareInstallation,
@ -39,7 +38,6 @@ defmodule FarmbotExt.API.DirtyWorker.Supervisor do
{DirtyWorker, DeviceCert},
{DirtyWorker, FbosConfig},
{DirtyWorker, FirmwareConfig},
{DirtyWorker, DiagnosticDump},
{DirtyWorker, FarmEvent},
{DirtyWorker, FarmwareEnv},
{DirtyWorker, FarmwareInstallation},

View File

@ -9,7 +9,6 @@ defmodule FarmbotExt.API.EagerLoader.Supervisor do
alias FarmbotCore.Asset.{
Device,
DiagnosticDump,
FarmEvent,
FarmwareEnv,
FirstPartyFarmware,
@ -42,7 +41,6 @@ defmodule FarmbotExt.API.EagerLoader.Supervisor do
def init(_args) do
children = [
{EagerLoader, Device},
{EagerLoader, DiagnosticDump},
{EagerLoader, FarmEvent},
{EagerLoader, FarmwareEnv},
{EagerLoader, FirstPartyFarmware},

View File

@ -3,6 +3,7 @@
# If you run "mix test --cover", coverage assets end up here.
/cover/
*.coverdata
# The directory Mix downloads your dependencies sources to.
/deps/
@ -23,4 +24,4 @@ erl_crash.dump
farmbot_firmware-*.tar
# Ignore vcr files
*.txt
*.txt

View File

@ -1,6 +1,6 @@
PREFIX=$(PWD)/priv
BUILD=$(PWD)/_build
ARDUINO_SRC_DIR=$(PWD)/c_src/farmbot-arduino-firmware
BUILD=$(ARDUINO_SRC_DIR)/_build
.PHONY: all clean fbos_arduino_firmware
@ -24,4 +24,4 @@ $(PREFIX):
mkdir -p $(PREFIX)
$(BUILD):
mkdir -p $(BUILD)
mkdir -p $(BUILD)

View File

@ -0,0 +1,6 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 55
}
}

View File

@ -166,6 +166,7 @@ defmodule FarmbotFirmware do
If the firmware is in any of the following states:
* `:boot`
* `:transport_boot`
* `:no_config`
* `:configuration`
`command` will fail with `{:error, state}`
@ -443,10 +444,13 @@ defmodule FarmbotFirmware do
# the `configuration_queue` and in the `command_queue`.
def handle_call(:close_transport, _from, %{status: s} = state)
when s != :transport_boot do
true = Process.demonitor(state.transport_ref)
if is_reference(state.transport_ref) do
true = Process.demonitor(state.transport_ref)
end
:ok = GenServer.stop(state.transport_pid, :normal)
state =
next_state =
goto(
%{
state
@ -459,7 +463,7 @@ defmodule FarmbotFirmware do
:transport_boot
)
{:reply, :ok, state}
{:reply, :ok, next_state}
end
def handle_call(:close_transport, _, %{status: s} = state) do
@ -501,8 +505,14 @@ defmodule FarmbotFirmware do
end
end
def handle_call({_tag, _code} = gcode, from, state) do
handle_command(gcode, from, state)
def handle_call({tag, {kind, args}}, from, state) do
handle_command({tag, {kind, args}}, from, state)
end
# TODO(RICK): Not sure if this is required.
# Some commands were missing a tag.
def handle_call({kind, args}, from, state) do
handle_command({nil, {kind, args}}, from, state)
end
@doc false
@ -542,7 +552,7 @@ defmodule FarmbotFirmware do
# If not in an acceptable state, return an error immediately.
def handle_command(_, _, %{status: s} = state)
when s in [:transport_boot, :boot, :no_config, :configuration] do
{:reply, {:error, s}, state}
{:reply, {:error, "Can't send command when in #{inspect(s)} state"}, state}
end
def handle_command({tag, {_, _}} = code, {pid, _ref}, state) do

View File

@ -93,9 +93,9 @@ defmodule FarmbotFirmware.Command do
Application.put_env(:farmbot_firmware, __MODULE__, new)
end
defp debug?() do
def debug?() do
Application.get_env(:farmbot_firmware, __MODULE__)[:debug_log] || false
end
defp debug_log(msg), do: if(debug?(), do: Logger.debug(msg), else: :ok)
def debug_log(msg), do: if(debug?(), do: Logger.debug(msg), else: :ok)
end

View File

@ -164,15 +164,15 @@ defmodule FarmbotFirmware.GCODE.Decoder do
end
@spec decode_end_stops([binary()], Keyword.t()) :: Keyword.t()
defp decode_end_stops(list, acc \\ [])
def decode_end_stops(list, acc \\ [])
defp decode_end_stops(
[
<<arg::binary-1, "A", val0::binary>>,
<<arg::binary-1, "B", val1::binary>> | rest
],
acc
) do
def decode_end_stops(
[
<<arg::binary-1, "A", val0::binary>>,
<<arg::binary-1, "B", val1::binary>> | rest
],
acc
) do
dc = String.downcase(arg)
acc =
@ -185,19 +185,18 @@ defmodule FarmbotFirmware.GCODE.Decoder do
decode_end_stops(rest, acc)
end
defp decode_end_stops([], acc), do: acc
def decode_end_stops([], acc), do: acc
def decode_end_stops(error, _acc), do: [parse_error: error]
defp decode_end_stops(error, _acc), do: [parse_error: error]
defp decode_pv(["P" <> param_id, "V" <> value]) do
def decode_pv(["P" <> param_id, "V" <> value]) do
param = Param.decode(String.to_integer(param_id))
{value, ""} = Float.parse(value)
[{param, value}]
end
defp decode_ints(pvm, acc \\ [])
def decode_ints(pvm, acc \\ [])
defp decode_ints([<<arg::binary-1, val::binary>> | rest], acc) do
def decode_ints([<<arg::binary-1, val::binary>> | rest], acc) do
arg =
arg
|> String.downcase()
@ -209,7 +208,7 @@ defmodule FarmbotFirmware.GCODE.Decoder do
end
end
defp decode_ints([], acc), do: Enum.reverse(acc)
def decode_ints([], acc), do: Enum.reverse(acc)
@spec decode_echo(binary()) :: [binary()]
defp decode_echo(str) when is_binary(str) do

View File

@ -27,6 +27,11 @@ defmodule FarmbotFirmware.PackageUtils do
|> assert_exists()
end
def find_hex_file("none") do
Application.app_dir(:farmbot_firmware, ["priv", "eeprom_clear.ino.hex"])
|> assert_exists()
end
def find_hex_file(hardware) when is_binary(hardware),
do: {:error, "unknown firmware hardware: #{hardware}"}

View File

@ -226,6 +226,7 @@ defmodule FarmbotFirmware.Param do
@seconds "(seconds)"
@steps "(steps)"
@steps_per_mm "(steps/mm)"
@steps_per_s "(steps/s)"
@milliamps "(milliamps)"
@doc "Translates a param to a human readable string"
@ -349,31 +350,31 @@ defmodule FarmbotFirmware.Param do
do: {"steps per mm, z-axis", @steps_per_mm, format_float(value)}
def to_human(:movement_min_spd_x, value),
do: {"minimum speed, x-axis", @steps_per_mm, format_float(value)}
do: {"minimum speed, x-axis", @steps_per_s, format_float(value)}
def to_human(:movement_min_spd_y, value),
do: {"minimum speed, y-axis", @steps_per_mm, format_float(value)}
do: {"minimum speed, y-axis", @steps_per_s, format_float(value)}
def to_human(:movement_min_spd_z, value),
do: {"minimum speed, z-axis", @steps_per_mm, format_float(value)}
do: {"minimum speed, z-axis", @steps_per_s, format_float(value)}
def to_human(:movement_home_spd_x, value),
do: {"homing speed, x-axis", @steps_per_mm, format_float(value)}
do: {"homing speed, x-axis", @steps_per_s, format_float(value)}
def to_human(:movement_home_spd_y, value),
do: {"homing speed, y-axis", @steps_per_mm, format_float(value)}
do: {"homing speed, y-axis", @steps_per_s, format_float(value)}
def to_human(:movement_home_spd_z, value),
do: {"homing speed, z-axis", @steps_per_mm, format_float(value)}
do: {"homing speed, z-axis", @steps_per_s, format_float(value)}
def to_human(:movement_max_spd_x, value),
do: {"max speed, x-axis", @steps_per_mm, format_float(value)}
do: {"max speed, x-axis", @steps_per_s, format_float(value)}
def to_human(:movement_max_spd_y, value),
do: {"max speed, y-axis", @steps_per_mm, format_float(value)}
do: {"max speed, y-axis", @steps_per_s, format_float(value)}
def to_human(:movement_max_spd_z, value),
do: {"max speed, z-axis", @steps_per_mm, format_float(value)}
do: {"max speed, z-axis", @steps_per_s, format_float(value)}
def to_human(:movement_invert_2_endpoints_x, value),
do: {"invert endstops, x-axis", nil, format_bool(value)}
@ -412,13 +413,13 @@ defmodule FarmbotFirmware.Param do
do: {"microsteps, z-axis", nil, format_float(value)}
def to_human(:encoder_enabled_x, value),
do: {"enable encoders, x-axis", nil, format_bool(value)}
do: {"enable encoders / stall detection, x-axis", nil, format_bool(value)}
def to_human(:encoder_enabled_y, value),
do: {"enable encoders, y-axis", nil, format_bool(value)}
do: {"enable encoders / stall detection, y-axis", nil, format_bool(value)}
def to_human(:encoder_enabled_z, value),
do: {"enable encoders, z-axis", nil, format_bool(value)}
do: {"enable encoders / stall detection, z-axis", nil, format_bool(value)}
def to_human(:encoder_type_x, value),
do: {"encoder type, x-axis", nil, format_float(value)}
@ -430,13 +431,13 @@ defmodule FarmbotFirmware.Param do
do: {"encoder type, z-axis", nil, format_float(value)}
def to_human(:encoder_missed_steps_max_x, value),
do: {"max missed steps, x-axis", nil, format_float(value)}
do: {"max missed steps, x-axis", @steps, format_float(value)}
def to_human(:encoder_missed_steps_max_y, value),
do: {"max missed steps, y-axis", nil, format_float(value)}
do: {"max missed steps, y-axis", @steps, format_float(value)}
def to_human(:encoder_missed_steps_max_z, value),
do: {"max missed steps, z-axis", nil, format_float(value)}
do: {"max missed steps, z-axis", @steps, format_float(value)}
def to_human(:encoder_scaling_x, value),
do: {"encoder scaling, x-axis", nil, format_float(value)}
@ -448,13 +449,13 @@ defmodule FarmbotFirmware.Param do
do: {"encoder scaling, z-axis", nil, format_float(value)}
def to_human(:encoder_missed_steps_decay_x, value),
do: {"encoder missed steps decay, x-axis", nil, format_float(value)}
do: {"missed step decay, x-axis", @steps, format_float(value)}
def to_human(:encoder_missed_steps_decay_y, value),
do: {"encoder missed steps decay, y-axis", nil, format_float(value)}
do: {"missed step decay, y-axis", @steps, format_float(value)}
def to_human(:encoder_missed_steps_decay_z, value),
do: {"encoder missed steps decay, z-axis", nil, format_float(value)}
do: {"missed step decay, z-axis", @steps, format_float(value)}
def to_human(:encoder_use_for_pos_x, value),
do: {"use encoders for positioning, x-axis", nil, format_bool(value)}

View File

@ -11,26 +11,24 @@ defmodule FarmbotFirmware.Request do
{:ok, GCODE.t()}
| {:error,
:invalid_command | :firmware_error | FarmbotFirmware.status()}
@whitelist [
:parameter_read,
:status_read,
:pin_read,
:end_stops_read,
:position_read,
:software_version_read
]
def request(firmware_server \\ FarmbotFirmware, code)
def request(firmware_server, {_tag, {kind, _}} = code) do
if kind not in [
:parameter_read,
:status_read,
:pin_read,
:end_stops_read,
:position_read,
:software_version_read
] do
if kind not in @whitelist do
raise ArgumentError, "#{kind} is not a valid request."
end
case GenServer.call(firmware_server, code, :infinity) do
{:ok, tag} ->
wait_for_request_result(tag, code)
{:error, status} ->
{:error, status}
{:ok, tag} -> wait_for_request_result(tag, code)
{:error, status} -> {:error, status}
end
end
@ -38,16 +36,26 @@ defmodule FarmbotFirmware.Request do
request(firmware_server, {to_string(:rand.uniform(100)), code})
end
def request_timeout(tag, code, result \\ nil) do
if result do
{:ok, {tag, result}}
else
{:error, "timeout waiting for request to complete: #{inspect(code)}"}
end
end
# This is a bit weird but let me explain:
# if this function `receive`s
# * report_error
# * report_invalid
# * report_emergency_lock
# it needs to return an error.
#
# If this function `receive`s
# * report_success
# when no valid data has been collected from `wait_for_request_result_process`
# it needs to return an error.
#
# If this function `receive`s
# * report_success
# when valid data has been collected from `wait_for_request_result_process`
@ -81,10 +89,7 @@ defmodule FarmbotFirmware.Request do
{tag, report} ->
wait_for_request_result_process(report, tag, code, result)
after
10_000 ->
if result,
do: {:ok, {tag, result}},
else: {:error, "timeout waiting for request to complete"}
10_000 -> request_timeout(tag, code, result)
end
end

View File

@ -215,7 +215,7 @@ defmodule FarmbotFirmware.StubTransport do
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_software_version, ["8.0.0.S"]),
GCODE.new(:report_software_version, ["8.0.0.S.stub"]),
GCODE.new(:report_success, [], tag)
]

View File

@ -14,7 +14,7 @@ defmodule FarmbotFirmware.UARTTransport do
device = Keyword.fetch!(args, :device)
handle_gcode = Keyword.fetch!(args, :handle_gcode)
reset = Keyword.get(args, :reset)
{:ok, uart} = uart_adapter().start_link()
{:ok, uart} = UartDefaultAdapter.start_link()
{:ok,
%{
@ -27,11 +27,11 @@ defmodule FarmbotFirmware.UARTTransport do
end
def terminate(_, %{uart: uart}) do
uart_adapter().stop(uart)
UartDefaultAdapter.stop(uart)
end
def handle_info(:timeout, %{open: false} = state) do
opts = uart_adapter().generate_opts()
opts = UartDefaultAdapter.generate_opts()
with :ok <- open(state.uart, state.device, opts),
:ok <- reset(state) do
@ -55,7 +55,7 @@ defmodule FarmbotFirmware.UARTTransport do
def handle_call(code, _from, state) do
str = GCODE.encode(code)
r = uart_adapter().write(state.uart, str)
r = UartDefaultAdapter.write(state.uart, str)
{:reply, r, state}
end
@ -68,10 +68,6 @@ defmodule FarmbotFirmware.UARTTransport do
end
def open(uart_pid, device_path, opts) do
uart_adapter().open(uart_pid, device_path, opts)
end
def uart_adapter() do
UartDefaultAdapter
UartDefaultAdapter.open(uart_pid, device_path, opts)
end
end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
defmodule FarmbotFirmware.CommandTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
alias FarmbotFirmware.Command
import ExUnit.CaptureLog
@subject FarmbotFirmware.Command
test "command() runs RPCs" do
arg = [transport: FarmbotFirmware.StubTransport]
{:ok, pid} = FarmbotFirmware.start_link(arg, [])
send(pid, :timeout)
assert {:error, :emergency_lock} ==
FarmbotFirmware.command(pid, {:command_emergency_lock, []})
assert :ok == FarmbotFirmware.command(pid, {:command_emergency_unlock, []})
cmd = {:parameter_write, [movement_stop_at_home_x: 0.0]}
assert :ok == FarmbotFirmware.command(pid, cmd)
end
test "command() refuses to run RPCs in :boot state" do
arg = [transport: FarmbotFirmware.StubTransport]
{:ok, pid} = FarmbotFirmware.start_link(arg, [])
send(pid, :timeout)
{:error, message} = @subject.command(pid, {:a, {:b, :c}})
assert "Can't send command when in :boot state" == message
end
test "enable_debug_logs" do
Application.put_env(:farmbot_firmware, @subject, foo: :bar, debug_log: false)
old_env = Application.get_env(:farmbot_firmware, @subject)
assert false == Keyword.fetch!(old_env, :debug_log)
assert :bar == Keyword.fetch!(old_env, :foo)
assert false == @subject.debug?()
refute capture_log(fn ->
@subject.debug_log("Never Shown")
end) =~ "Never Shown"
# === Change ENV settings
assert :ok ==
Command.enable_debug_logs()
assert capture_log(fn ->
@subject.debug_log("Good!")
end) =~ "Good!"
assert true == @subject.debug?()
new_env = Application.get_env(:farmbot_firmware, @subject)
assert true == Keyword.fetch!(new_env, :debug_log)
assert :bar == Keyword.fetch!(new_env, :foo)
# === And back again
assert :ok == Command.disable_debug_logs()
even_newer = Application.get_env(:farmbot_firmware, @subject)
assert false == Keyword.fetch!(even_newer, :debug_log)
assert :bar == Keyword.fetch!(even_newer, :foo)
assert false == @subject.debug?()
refute capture_log(fn ->
@subject.debug_log("Also Never Shown")
end) =~ "Also Never Shown"
end
end

View File

@ -0,0 +1,35 @@
defmodule FarmbotFirmware.GCODE.DecoderTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
alias FarmbotFirmware.GCODE.Decoder
test "Decoder.decode_ints(pvm, acc \\ [])" do
assert [a: 1, b: 2, c: 3] == Decoder.decode_ints(["A1", "B2", "C3"])
end
test "Decoder.decode_pv" do
assert [param_config_ok: 3.0] == Decoder.decode_pv(["P2", "V3"])
end
# NOTE: Theese values are totally random and may
# not represent real-world use of the GCode.
test "Decoder.decode_floats" do
assert {:command_movement, []} == Decoder.do_decode("G00", ["XA0.0"])
assert {:report_load, [x: 0.0]} == Decoder.do_decode("R89", ["X0.0"])
assert {:report_encoders_raw, [x: 0.0]} == Decoder.do_decode("R85", ["X0"])
assert {:report_encoders_scaled, []} == Decoder.do_decode("R84", ["XA-0.0"])
assert {:report_position, []} == Decoder.do_decode("R82", ["XA-0"])
assert {:report_position_change, []} == Decoder.do_decode("R17", ["XB1.0"])
assert {:report_position_change, []} == Decoder.do_decode("R16", ["XB-1.0"])
assert {:report_position_change, []} == Decoder.do_decode("R15", ["XB10"])
assert {:report_position_change, [x: 1.0]} ==
Decoder.do_decode("R15", ["X1"])
assert {:report_position_change, []} == Decoder.do_decode("R16", ["YA1"])
assert {:command_movement, []} == Decoder.do_decode("G00", ["YB1"])
assert {:report_load, []} == Decoder.do_decode("R89", ["ZA1"])
assert {:report_position, []} == Decoder.do_decode("R82", ["ZB1"])
end
end

View File

@ -0,0 +1,21 @@
defmodule FarmbotFirmware.RequestTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
alias FarmbotFirmware.Request
test "request whitelist" do
boom = fn ->
Request.request({:a, {:b, :c}})
end
assert_raise ArgumentError, boom
end
test "timeout handler" do
ok = Request.request_timeout(:tag, :code, :result)
error = Request.request_timeout(:tag, :code)
assert {:ok, {:tag, :result}} == ok
assert {:error, "timeout waiting for request to complete: :code"} == error
end
end

View File

@ -2,15 +2,55 @@ defmodule FarmbotFirmwareTest do
use ExUnit.Case
doctest FarmbotFirmware
test "initialization (WIP)" do
pid =
start_supervised!({
FarmbotFirmware,
transport: FarmbotFirmware.StubTransport,
side_effects: FarmbotCore.FirmwareSideEffects
})
def try_command(pid, cmd) do
GenServer.call(pid, cmd, :infinity)
end
# WIP
assert is_pid(pid)
def firmware_server do
arg = [transport: FarmbotFirmware.StubTransport]
{:ok, pid} = FarmbotFirmware.start_link(arg, [])
send(pid, :timeout)
try_command(pid, {nil, {:command_emergency_lock, []}})
try_command(pid, {nil, {:command_emergency_unlock, []}})
pid
end
test "various command()s" do
pid = firmware_server()
cmds = [
# TEST VALUES EXTRACTED FROM A REAL BOT
# They may change over time, so a test failure
# is not necessarily indicitive of a defect,
# but it *IS* indicitve of a change (perhaps unepxected?)
# in runtime behavior.
#
# Approach with caution.
{:pin_mode_write, [p: 13, m: 1]},
{"1", {:position_write_zero, [:x]}},
{"23", {:parameter_write, [movement_invert_2_endpoints_y: 1.0]}},
{"24", {:parameter_write, [movement_stop_at_home_x: 0.0]}},
{"40", {:pin_write, [p: 13, v: 0, m: 0]}},
{"49", {:pin_mode_write, [p: 13, m: 1]}},
{"55", {:pin_mode_write, [p: 13, m: 1]}},
{"59", {:pin_mode_write, [p: 13, m: 1]}},
{"94", {:parameter_write, [movement_home_up_y: 1.0]}},
{"94", {:pin_write, [p: 13, v: 1, m: 0]}},
{"98", {:parameter_write, [movement_home_up_y: 0.0]}},
{"99", {:parameter_write, [movement_stop_at_home_x: 1.0]}},
{nil, {:command_emergency_lock, []}},
{nil, {:command_emergency_lock, []}},
{nil,
{:command_movement,
[x: 0.0, y: 0.0, z: 0.0, a: 400.0, b: 400.0, c: 400.0]}},
{nil,
{:command_movement,
[x: 0.0, y: 0.0, z: 10.0, a: 400.0, b: 400.0, c: 400.0]}},
{nil, {:command_emergency_lock, []}}
]
Enum.map(cmds, fn {tag, cmd} ->
assert {:ok, tag} = try_command(pid, {tag, cmd})
end)
end
end

View File

@ -383,6 +383,81 @@ defmodule FarmbotFirmware.GCODETest do
assert "R06 Z2 Q1" =
GCODE.encode({"1", {:report_calibration_state, [z: :end]}})
assert "R15 X200.00" ==
GCODE.encode({nil, {:report_position_change, [{:x, 200.0}]}})
assert "R16 Y12.00" ==
GCODE.encode({nil, {:report_position_change, [{:y, 12.0}]}})
assert "R16 Z37.02" ==
GCODE.encode({nil, {:report_position_change, [{:z, 37.02}]}})
assert "R20" == GCODE.encode({nil, {:report_parameters_complete, []}})
assert "R21 P0 V1.20" ==
GCODE.encode(
{nil, {:report_parameter_value, [{:param_version, 1.2}]}}
)
assert "R41 P1 V2 M0 Q3" ==
GCODE.encode(
{nil,
{:report_pin_value, [{:p, 1}, {:v, 2}, {:m, 0}, {:q, 3}]}}
)
assert "R23 P0 V1.20" ==
GCODE.encode(
{nil,
{:report_calibration_parameter_value, [{:param_version, 1.2}]}}
)
enc_params = [xa: 1, xb: 2, ya: 3, yb: 4, za: 5, zb: 6]
assert "R81 XA1 XB2 YA3 YB4 ZA5 ZB6" ==
GCODE.encode({nil, {:report_end_stops, enc_params}})
pos_params = [x: 1.4, y: 2.3, z: 3.2, s: 4.1]
assert "R82 X1.40 Y2.30 Z3.20 S4.10" ==
GCODE.encode({nil, {:report_position, pos_params}})
params = [x: 1.4, y: 2.3, z: 3.2]
assert "R84 X1.40 Y2.30 Z3.20" ==
GCODE.encode({nil, {:report_encoders_scaled, params}})
params = [x: 1.4, y: 2.3, z: 3.2]
assert "R85 X1.40 Y2.30 Z3.20" ==
GCODE.encode({nil, {:report_encoders_raw, params}})
params = [x: 1.4, y: 2.3, z: 3.2]
assert "R89 X1.40 Y2.30 Z3.20" ==
GCODE.encode({nil, {:report_load, params}})
assert "G00 X0.00" ==
GCODE.encode({nil, {:command_movement_home, [:x]}})
assert "G00 Y0.00" ==
GCODE.encode({nil, {:command_movement_home, [:y]}})
assert "G00 Z0.00" ==
GCODE.encode({nil, {:command_movement_home, [:z]}})
assert(
"F21 P222" ==
GCODE.encode({nil, {:parameter_read, [:pin_guard_5_time_out]}})
)
p = {:calibration_parameter_write, [{:pin_guard_5_time_out, 1.2}]}
assert "F23 P222 V1.20" ==
GCODE.encode({nil, p})
assert "F42 P13" == GCODE.encode({nil, {:pin_read, [p: 13]}})
assert "F61 P54" == GCODE.encode({nil, {:servo_write, [p: 54]}})
end
test "retry" do

View File

@ -0,0 +1,55 @@
defmodule FarmbotFirmware.PackageUtilsTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
alias FarmbotFirmware.PackageUtils
test "package to string" do
assert PackageUtils.package_to_string("arduino") ==
"Arduino/RAMPS (Genesis v1.2)"
assert PackageUtils.package_to_string("farmduino") ==
"Farmduino (Genesis v1.3)"
assert PackageUtils.package_to_string("farmduino_k14") ==
"Farmduino (Genesis v1.4)"
assert PackageUtils.package_to_string("farmduino_k15") ==
"Farmduino (Genesis v1.5)"
assert PackageUtils.package_to_string("express_k10") ==
"Farmduino (Express v1.0)"
assert PackageUtils.package_to_string("misc") == "misc"
end
test "not finding files" do
expect(File, :exists?, 1, fn _fname ->
false
end)
{:error, error} = PackageUtils.find_hex_file("arduino")
assert String.contains?(error, "Please call `make arudino_firmware`.")
end
test "finding files" do
{:ok, path} = PackageUtils.find_hex_file("arduino")
assert String.contains?(path, "/farmbot_firmware/priv/arduino_firmware.hex")
{:ok, path} = PackageUtils.find_hex_file("farmduino")
assert String.contains?(path, "/farmbot_firmware/priv/farmduino.hex")
{:ok, path} = PackageUtils.find_hex_file("farmduino_k14")
assert String.contains?(path, "/farmbot_firmware/priv/farmduino_k14.hex")
{:ok, path} = PackageUtils.find_hex_file("farmduino_k15")
assert String.contains?(path, "/farmbot_firmware/priv/farmduino_k15.hex")
{:ok, path} = PackageUtils.find_hex_file("express_k10")
assert String.contains?(path, "/farmbot_firmware/priv/express_k10.hex")
assert {:error, "unknown firmware hardware: no"} ==
PackageUtils.find_hex_file("no")
end
end

View File

@ -0,0 +1,220 @@
defmodule FarmbotFirmware.ParamTest do
use ExUnit.Case
alias FarmbotFirmware.Param
test "to_human()" do
float_value = 1.23
seconds = "(seconds)"
steps = "(steps)"
steps_per_s = "(steps/s)"
steps_per_mm = "(steps/mm)"
assert Param.to_human(:param_test, 1) ==
{"param_test", nil, true}
assert Param.to_human(:param_config_ok, 1) ==
{"param_config_ok", nil, true}
assert Param.to_human(:param_use_eeprom, 1) ==
{"use eeprom", nil, true}
assert Param.to_human(:param_e_stop_on_mov_err, 1) ==
{"e-stop on movement errors", nil, true}
assert Param.to_human(:movement_timeout_x, float_value) ==
{"timeout after, x-axis", seconds, "1.2"}
assert Param.to_human(:movement_timeout_y, float_value) ==
{"timeout after, y-axis", seconds, "1.2"}
assert Param.to_human(:movement_timeout_z, float_value) ==
{"timeout after, z-axis", seconds, "1.2"}
assert Param.to_human(:movement_keep_active_x, 1) ==
{"always power motors, x-axis", nil, true}
assert Param.to_human(:movement_keep_active_y, 1) ==
{"always power motors, y-axis", nil, true}
assert Param.to_human(:movement_keep_active_z, 1) ==
{"always power motors, z-axis", nil, true}
assert Param.to_human(:movement_home_at_boot_x, 1) ==
{"find home on boot, x-axis", nil, true}
assert Param.to_human(:movement_home_at_boot_y, 1) ==
{"find home on boot, y-axis", nil, true}
assert Param.to_human(:movement_home_at_boot_z, 1) ==
{"find home on boot, z-axis", nil, true}
assert Param.to_human(:movement_invert_endpoints_x, 1) ==
{"swap endstops, x-axis", nil, true}
assert Param.to_human(:movement_invert_endpoints_y, 1) ==
{"swap endstops, y-axis", nil, true}
assert Param.to_human(:movement_invert_endpoints_z, 1) ==
{"swap endstops, z-axis", nil, true}
assert Param.to_human(:movement_enable_endpoints_x, 1) ==
{"enable endstops, x-axis", nil, true}
assert Param.to_human(:movement_enable_endpoints_y, 1) ==
{"enable endstops, y-axis", nil, true}
assert Param.to_human(:movement_enable_endpoints_z, 1) ==
{"enable endstops, z-axis", nil, true}
assert Param.to_human(:movement_invert_motor_x, 1) ==
{"invert motor, x-axis", nil, true}
assert Param.to_human(:movement_invert_motor_y, 1) ==
{"invert motor, y-axis", nil, true}
assert Param.to_human(:movement_invert_motor_z, 1) ==
{"invert motor, z-axis", nil, true}
assert Param.to_human(:movement_secondary_motor_x, 1) ==
{"enable 2nd x motor", nil, true}
assert Param.to_human(:movement_secondary_motor_invert_x, 1) ==
{"invert 2nd x motor", nil, true}
assert Param.to_human(:movement_stop_at_home_x, 1) ==
{"stop at home, x-axis", nil, true}
assert Param.to_human(:movement_stop_at_home_y, 1) ==
{"stop at home, y-axis", nil, true}
assert Param.to_human(:movement_stop_at_home_z, 1) ==
{"stop at home, z-axis", nil, true}
assert Param.to_human(:movement_step_per_mm_x, float_value) ==
{"steps per mm, x-axis", steps_per_mm, "1.2"}
assert Param.to_human(:movement_step_per_mm_y, float_value) ==
{"steps per mm, y-axis", steps_per_mm, "1.2"}
assert Param.to_human(:movement_step_per_mm_z, float_value) ==
{"steps per mm, z-axis", steps_per_mm, "1.2"}
assert Param.to_human(:movement_min_spd_x, float_value) ==
{"minimum speed, x-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_min_spd_y, float_value) ==
{"minimum speed, y-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_min_spd_z, float_value) ==
{"minimum speed, z-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_home_spd_x, float_value) ==
{"homing speed, x-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_home_spd_y, float_value) ==
{"homing speed, y-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_home_spd_z, float_value) ==
{"homing speed, z-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_max_spd_x, float_value) ==
{"max speed, x-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_max_spd_y, float_value) ==
{"max speed, y-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_max_spd_z, float_value) ==
{"max speed, z-axis", steps_per_s, "1.2"}
assert Param.to_human(:movement_invert_2_endpoints_x, 1) ==
{"invert endstops, x-axis", nil, true}
assert Param.to_human(:movement_invert_2_endpoints_y, 1) ==
{"invert endstops, y-axis", nil, true}
assert Param.to_human(:movement_invert_2_endpoints_z, 1) ==
{"invert endstops, z-axis", nil, true}
assert Param.to_human(:encoder_enabled_x, 1) ==
{"enable encoders / stall detection, x-axis", nil, true}
assert Param.to_human(:encoder_enabled_y, 1) ==
{"enable encoders / stall detection, y-axis", nil, true}
assert Param.to_human(:encoder_enabled_z, 1) ==
{"enable encoders / stall detection, z-axis", nil, true}
assert Param.to_human(:encoder_type_x, float_value) ==
{"encoder type, x-axis", nil, "1.2"}
assert Param.to_human(:encoder_type_y, float_value) ==
{"encoder type, y-axis", nil, "1.2"}
assert Param.to_human(:encoder_type_z, float_value) ==
{"encoder type, z-axis", nil, "1.2"}
assert Param.to_human(:encoder_scaling_x, float_value) ==
{"encoder scaling, x-axis", nil, "1.2"}
assert Param.to_human(:encoder_scaling_y, float_value) ==
{"encoder scaling, y-axis", nil, "1.2"}
assert Param.to_human(:encoder_scaling_z, float_value) ==
{"encoder scaling, z-axis", nil, "1.2"}
assert Param.to_human(:encoder_missed_steps_decay_x, float_value) ==
{"missed step decay, x-axis", steps, "1.2"}
assert Param.to_human(:encoder_missed_steps_decay_y, float_value) ==
{"missed step decay, y-axis", steps, "1.2"}
assert Param.to_human(:encoder_missed_steps_decay_z, float_value) ==
{"missed step decay, z-axis", steps, "1.2"}
assert Param.to_human(:encoder_use_for_pos_x, 1) ==
{"use encoders for positioning, x-axis", nil, true}
assert Param.to_human(:encoder_use_for_pos_y, 1) ==
{"use encoders for positioning, y-axis", nil, true}
assert Param.to_human(:encoder_use_for_pos_z, 1) ==
{"use encoders for positioning, z-axis", nil, true}
assert Param.to_human(:encoder_invert_x, 1) ==
{"invert encoders, x-axis", nil, true}
assert Param.to_human(:encoder_invert_y, 1) ==
{"invert encoders, y-axis", nil, true}
assert Param.to_human(:encoder_invert_z, 1) ==
{"invert encoders, z-axis", nil, true}
assert Param.to_human(:movement_stop_at_max_x, 1) ==
{"stop at max, x-axis", nil, true}
assert Param.to_human(:movement_stop_at_max_y, 1) ==
{"stop at max, y-axis", nil, true}
assert Param.to_human(:movement_stop_at_max_z, 1) ==
{"stop at max, z-axis", nil, true}
assert Param.to_human(:pin_guard_1_active_state, 0) ==
{"pin guard 1 safe state", nil, "HIGH"}
assert Param.to_human(:pin_guard_2_active_state, 0) ==
{"pin guard 2 safe state", nil, "HIGH"}
assert Param.to_human(:pin_guard_3_active_state, 0) ==
{"pin guard 3 safe state", nil, "HIGH"}
assert Param.to_human(:pin_guard_4_active_state, 0) ==
{"pin guard 4 safe state", nil, "HIGH"}
assert Param.to_human(:pin_guard_5_active_state, 0) ==
{"pin guard 5 safe state", nil, "HIGH"}
end
test "Handling of uknown parameters" do
assert :unknown_parameter == Param.decode(-999)
end
end

View File

@ -1,4 +1,5 @@
Application.ensure_all_started(:mimic)
Mimic.copy(FarmbotFirmware.UartDefaultAdapter)
Mimic.copy(Circuits.UART)
Mimic.copy(File)
ExUnit.start()

View File

@ -18,4 +18,5 @@ erl_crash.dump
*.sqlite3
auth_secret.exs
log
*.csv
*.csv
*.coverdata

View File

@ -34,7 +34,6 @@ config :nerves, :firmware,
config :farmbot_core, FarmbotCore.AssetMonitor, checkup_time_ms: 5_000
config :farmbot_core, FarmbotCore.EctoMigrator,
expected_fw_versions: ["6.4.2.F", "6.4.2.R", "6.4.2.G"],
default_firmware_io_logs: false,
default_server: "https://my.farm.bot",
default_ntp_server_1: "0.pool.ntp.org",
@ -74,9 +73,7 @@ config :farmbot, FarmbotOS.System,
system_tasks: FarmbotOS.Platform.Host.SystemTasks
config :farmbot, FarmbotOS.Configurator,
data_layer: FarmbotOS.Configurator.ConfigDataLayer,
network_layer: FarmbotOS.Configurator.FakeNetworkLayer,
telemetry_layer: FarmbotOS.Configurator.DetsTelemetryLayer
network_layer: FarmbotOS.Configurator.FakeNetworkLayer
config :farmbot, FarmbotOS.Platform.Supervisor,
platform_children: [

View File

@ -43,4 +43,3 @@ config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 0
config :plug, :validate_header_keys_during_test, true
config :farmbot, :muon_trap_adapter, Avrdude.MuonTrapTestAdapter

View File

@ -0,0 +1,6 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 29
}
}

View File

@ -31,7 +31,7 @@ defmodule Avrdude do
# call the function for resetting the line before executing avrdude.
call_reset_fun(reset_fun)
Avrdude.MuonTrapAdapter.cmd("avrdude", args,
MuonTrap.cmd("avrdude", args,
into: IO.stream(:stdio, :line),
stderr_to_stdout: true
)

View File

@ -1,21 +0,0 @@
defmodule Avrdude.MuonTrapAdapter do
@type exe :: String.t()
@type args :: [String.t()]
@type io_stream :: Enumerable.t()
@type option :: {:into, io_stream()} | {:stderr_to_stdout, boolean()}
@callback cmd(exe, args, list(option)) :: {String.t(), non_neg_integer}
@doc false
def adapter do
Application.get_env(
:farmbot,
:muon_trap_adapter,
Avrdude.MuonTrapDefaultAdapter
)
end
def cmd(exe, args, options) do
adapter().cmd(exe, args, options)
end
end

View File

@ -1,6 +0,0 @@
defmodule Avrdude.MuonTrapDefaultAdapter do
@behaviour Avrdude.MuonTrapAdapter
@impl Avrdude.MuonTrapAdapter
defdelegate cmd(exe, args, options), to: MuonTrap, as: :cmd
end

View File

@ -7,6 +7,8 @@ defmodule FarmbotOS.Configurator.Router do
import Phoenix.HTML
use Plug.Router
use Plug.Debugger, otp_app: :farmbot
alias FarmbotOS.Configurator.ConfigDataLayer
plug(Plug.Logger)
plug(Plug.Static, from: {:farmbot, "priv/static"}, at: "/")
plug(Plug.Parsers, parsers: [:urlencoded, :multipart])
@ -21,13 +23,10 @@ defmodule FarmbotOS.Configurator.Router do
plug(:match)
plug(:dispatch)
@data_layer Application.get_env(:farmbot, FarmbotOS.Configurator)[:data_layer]
@network_layer Application.get_env(:farmbot, FarmbotOS.Configurator)[
:network_layer
]
@telemetry_layer Application.get_env(:farmbot, FarmbotOS.Configurator)[
:telemetry_layer
]
@telemetry_layer FarmbotOS.Configurator.DetsTelemetryLayer
# Trigger for captive portal for various operating systems
get("/gen_204", do: redir(conn, "/"))
@ -270,6 +269,11 @@ defmodule FarmbotOS.Configurator.Router do
get "/finish" do
FarmbotCore.Logger.debug(1, "Configuration complete")
# TODO(Rick): This pattern match is not 100% accurate.
# TO see what I mean, try calling `save_config/1` with
# _only_ the parameters provided in the line below-
# it will crash as it is missing numerous keys.
# It might be good to add an error page or something.
case get_session(conn) do
%{
"ifname" => _,
@ -277,10 +281,12 @@ defmodule FarmbotOS.Configurator.Router do
"auth_config_password" => _,
"auth_config_server" => _
} ->
FarmbotCore.Logger.debug(1, "Configuration success!")
save_config(get_session(conn))
render_page(conn, "finish")
_ ->
FarmbotCore.Logger.debug(1, "Configuration FAIL")
redir(conn, "/")
end
end
@ -355,23 +361,23 @@ defmodule FarmbotOS.Configurator.Router do
end
defp load_last_reset_reason do
@data_layer.load_last_reset_reason()
ConfigDataLayer.load_last_reset_reason()
end
defp load_email do
@data_layer.load_email()
ConfigDataLayer.load_email()
end
defp load_password do
@data_layer.load_password()
ConfigDataLayer.load_password()
end
def load_server do
@data_layer.load_server()
ConfigDataLayer.load_server()
end
defp save_config(conf) do
@data_layer.save_config(conf)
ConfigDataLayer.save_config(conf)
end
defp list_interfaces() do

View File

@ -18,7 +18,6 @@ defmodule FarmbotOS.SysCalls do
alias FarmbotOS.SysCalls.{
ChangeOwnership,
CheckUpdate,
DumpInfo,
Farmware,
FactoryReset,
FlashFirmware,
@ -52,9 +51,6 @@ defmodule FarmbotOS.SysCalls do
@impl true
defdelegate change_ownership(email, secret, server), to: ChangeOwnership
@impl true
defdelegate dump_info(), to: DumpInfo
@impl true
defdelegate check_update(), to: CheckUpdate

View File

@ -1,104 +0,0 @@
defmodule FarmbotOS.SysCalls.DumpInfo do
@moduledoc false
require FarmbotCore.Logger
require FarmbotTelemetry
alias FarmbotCore.{
Asset,
Asset.DiagnosticDump,
Asset.Private,
Config,
Project
}
def dump_info do
FarmbotCore.Logger.busy(1, "Recording diagnostic dump.")
ifname = get_network_config()
dmesg = dmesg()
fbos_commit = Project.commit()
fbos_version = Project.version()
fw_version = fw_version()
fw_commit = Project.arduino_commit()
fw_hardware = extract_fw_hardware(fw_version)
fw_data = fw_state(fw_version, fw_hardware)
params = %{
network_interface: ifname,
firmware_hardware: fw_hardware,
firmware_commit: fw_commit,
fbos_commit: fbos_commit,
fbos_version: fbos_version,
fbos_dmesg_dump: dmesg,
firmware_state: FarmbotCore.JSON.encode!(fw_data)
}
case Asset.new_diagnostic_dump(params) do
{:ok, diag} ->
_ = Private.mark_dirty!(diag, %{})
FarmbotCore.Logger.success(1, "Diagnostic dump recorded.")
FarmbotTelemetry.event(:diagnostic_dump, :record, nil,
diagnostic_dump: DiagnosticDump.render(diag)
)
:ok
{:error, changeset} ->
FarmbotTelemetry.event(:diagnostic_dump, :record_error, nil,
error: inspect(changeset)
)
{:error, "error creating diagnostic dump: #{inspect(changeset)}"}
end
end
defp get_network_config do
case Config.get_all_network_configs() do
[%{name: ifname} | _] -> ifname
_ -> nil
end
end
defp dmesg do
{dmesg, _status} = System.cmd("dmesg", [])
dmesg
end
defp fw_version do
case FarmbotFirmware.request({:software_version_read, []}) do
{:ok, {_, {:report_software_version, [version]}}} -> version
_ -> nil
end
end
defp extract_fw_hardware(nil), do: nil
defp extract_fw_hardware(str) do
case String.split(str, ".") do
[_, _, _, "R"] -> "arduino"
[_, _, _, "F"] -> "farmduino"
[_, _, _, "G"] -> "farmduino_k14"
[_, _, _, "H"] -> "farmduino_k15"
[_, _, _, "E"] -> "express_k10"
_ -> nil
end
end
defp fw_state(version, hardware) do
pid = Process.whereis(FarmbotFirmware)
if state = pid && :sys.get_state(pid) do
%{
firmware_hardware: hardware,
firmware_version: version,
busy: state.status == :busy,
serial_port: state.transport_args[:device],
locked: state.status == :emergency_lock,
current_command: inspect(state.command_queue)
}
else
%{error: "Firmware process is not running. Could not collect info."}
end
end
end

View File

@ -1,10 +1,10 @@
defmodule FarmbotOS.SysCalls.Farmware do
@moduledoc false
require Logger
# alias FarmbotCeleryScript.AST
require FarmbotCore.Logger
alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime}
alias FarmbotExt.API.ImageUploader
@farmware_timeout 30_000
def update_farmware(farmware_name) do
with {:ok, installation} <- lookup_installation(farmware_name) do
@ -48,16 +48,20 @@ defmodule FarmbotOS.SysCalls.Farmware do
def loop(farmware_runtime) do
receive do
{:error, :farmware_exit} ->
:ok
{:error, reason} ->
{:error, inspect(reason)}
{:error, :farmware_exit} -> :ok
{:error, reason} -> {:error, inspect(reason)}
after
30_000 ->
Logger.debug("Force stopping farmware: #{inspect(farmware_runtime)}")
FarmwareRuntime.stop(farmware_runtime)
:ok
@farmware_timeout -> farmware_timeout(farmware_runtime)
end
end
def farmware_timeout(farmware_runtime) do
time = @farmware_timeout / 1_000
runtime = inspect(farmware_runtime)
msg = "Farmware did not exit after #{time} seconds. Terminating #{runtime}"
FarmbotCore.Logger.info(2, msg)
FarmwareRuntime.stop(farmware_runtime)
:ok
end
end

View File

@ -32,10 +32,7 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
{_, 0} <- Avrdude.flash(hex_file, tty, fun) do
FarmbotCore.Logger.success(2, "Firmware flashed successfully!")
%{
# firmware_hardware: package,
firmware_path: tty
}
%{firmware_path: tty}
|> Asset.update_fbos_config!()
|> Private.mark_dirty!(%{})
@ -62,15 +59,19 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
end
end
defp find_reset_fun(_) do
config = Application.get_env(:farmbot_firmware, FarmbotFirmware)
defp find_reset_fun("express_k10") do
FarmbotCore.Logger.debug(3, "Using special reset function for express")
# "magic" workaround to avoid compiler warnings.
# 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
if module = config[:reset] do
Logger.error("using reset function: #{inspect(config)}")
{:ok, &module.reset/0}
else
Logger.error("no reset function is going to be used #{inspect(config)}")
{:ok, fn -> :ok end}
end
defp find_reset_fun(_) do
FarmbotCore.Logger.debug(3, "Using default reset function")
fun = &FarmbotFirmware.NullReset.reset/0
{:ok, fun}
end
end

View File

@ -69,39 +69,10 @@ defmodule FarmbotOS.SysCalls.Movement do
end
def move_absolute(x, y, z, speed) do
do_move_absolute(x, y, z, speed, max_retries())
do_move_absolute(x, y, z, speed)
end
defp do_move_absolute(x, y, z, speed, retries, errors \\ [])
# This is the final attempt before movement is aborted.
defp do_move_absolute(x, y, z, speed, 0, errors) do
with {:ok, speed_x} <- param_read(:movement_max_spd_x),
{:ok, speed_y} <- param_read(:movement_max_spd_y),
{:ok, speed_z} <- param_read(:movement_max_spd_z),
params <- [
x: x / 1.0,
y: y / 1.0,
z: z / 1.0,
a: speed / 100 * (speed_x || 1),
b: speed / 100 * (speed_y || 1),
c: speed / 100 * (speed_z || 1)
],
:ok <- FarmbotFirmware.command({nil, {:command_movement, params}}) do
:ok
else
{:error, reason} ->
errors =
[reason | errors]
|> Enum.reverse()
|> Enum.map(&inspect/1)
|> Enum.join(", ")
{:error, "movement error(s): #{errors}"}
end
end
defp do_move_absolute(x, y, z, speed, retries, errors) do
defp do_move_absolute(x, y, z, speed) do
with {:ok, speed_x} <- param_read(:movement_max_spd_x),
{:ok, speed_y} <- param_read(:movement_max_spd_y),
{:ok, speed_z} <- param_read(:movement_max_spd_z),
@ -120,15 +91,18 @@ defmodule FarmbotOS.SysCalls.Movement do
{:error, "emergency_lock"}
{:error, reason} ->
FarmbotCore.Logger.error(
1,
"Movement failed. Retrying up to #{retries} more time(s)"
)
handle_movement_error(reason)
do_move_absolute(x, y, z, speed, retries - 1, [reason | errors])
reason ->
handle_movement_error(reason)
end
end
def handle_movement_error(reason) do
msg = "Movement failed. #{inspect(reason)}"
FarmbotCore.Logger.error(1, msg)
end
def calibrate(axis) do
axis = assert_axis!(axis)
@ -173,13 +147,6 @@ defmodule FarmbotOS.SysCalls.Movement do
end
end
defp max_retries do
case param_read(:param_mov_nr_retry) do
{:ok, nr} -> floor(nr)
_ -> 3
end
end
defp assert_axis!(axis) when is_atom(axis),
do: axis
@ -187,7 +154,6 @@ defmodule FarmbotOS.SysCalls.Movement do
do: String.to_existing_atom(axis)
defp assert_axis!(axis) do
# {:error, "unknown axis #{axis}"}
raise("unknown axis #{axis}")
end
end

View File

@ -118,7 +118,7 @@ defmodule FarmbotOS.MixProject do
{:circuits_gpio, "~> 0.4.3", targets: @all_targets},
{:circuits_i2c, "~> 0.3.5", targets: @all_targets},
{:toolshed, "~> 0.2", targets: @all_targets},
{:vintage_net, "~> 0.7", targets: @all_targets},
{:vintage_net, "~> 0.7.5", targets: @all_targets},
{:vintage_net_ethernet, "~> 0.7.0", targets: @all_targets},
{:vintage_net_wifi, "~> 0.7.0", targets: @all_targets},
{:vintage_net_direct, "~> 0.7.0", targets: @all_targets},

View File

@ -45,7 +45,7 @@
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"},
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"},
"muontrap": {:hex, :muontrap, "0.5.0", "0b885a4095e990000d519441bccb8f037a9c4c35908720e7814a516a606be278", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"nerves": {:hex, :nerves, "1.5.3", "14abb71fa1ce0cd281ffb6ba743c6c896b664efc3c2dd542f8682a55602176d8", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.4", "12b0d9c84ec9f79c1b0ac0de1c575372ef972d0c58ce21c36bf354062c6222d9", [:mix], [{:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_hub": {:hex, :nerves_hub, "0.7.4", "0e104cad468c3d601ed423e116ea3422fbd31b7eedb263bcb2a5c489dca8b53b", [:mix], [{:fwup, "~> 0.3.0", [hex: :fwup, repo: "hexpm", optional: false]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nerves_hub_cli, "~> 0.9", [hex: :nerves_hub_cli, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:phoenix_client, "~> 0.7", [hex: :phoenix_client, repo: "hexpm", optional: false]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: false]}, {:x509, "~> 0.5", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"},
@ -89,7 +89,7 @@
"uboot_env": {:hex, :uboot_env, "0.1.1", "b01e3ec0973e99473234f27839e29e63b5b81eba6a136a18a78d049d4813d6c5", [:mix], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"},
"vintage_net": {:hex, :vintage_net, "0.7.2", "b14468e29704156f133a9342fd41c8cfc9bae0849bfa20a6859b29ac9e28166e", [:make, :mix], [{:busybox, "~> 0.1.4", [hex: :busybox, repo: "hexpm", optional: true]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:gen_state_machine, "~> 2.0.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5.0", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm"},
"vintage_net": {:hex, :vintage_net, "0.7.5", "4e843a3f029a32cd5644aa3397bbb1463e123b06848e5a3c99cefc58af395a2a", [:make, :mix], [{:busybox, "~> 0.1.4", [hex: :busybox, repo: "hexpm", optional: true]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:gen_state_machine, "~> 2.0.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5.1", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm"},
"vintage_net_direct": {:hex, :vintage_net_direct, "0.7.0", "6a8d95432fc2fb9a9e0225bf1a6dbb7c1ba097bb509a989e947ad3f44d2b0f28", [:mix], [{:one_dhcpd, "~> 0.2.3", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm"},
"vintage_net_ethernet": {:hex, :vintage_net_ethernet, "0.7.0", "b66a4da0846b4a55a471d15d8ab2bedbbef9c75c18f4e511c43777f7cfeffa09", [:mix], [{:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm"},
"vintage_net_wifi": {:hex, :vintage_net_wifi, "0.7.0", "be3d8275fa40ba357159033523606c74c8f612dbd60801840df60835453a6906", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm"},

View File

@ -1,7 +1,7 @@
defmodule FarmbotOS.Platform.Target.Configurator.Validator do
@moduledoc """
VintageNet.Technology that handles turning Farmbot's internal
network representation into either a VintageNetEthernet
VintageNet.Technology that handles turning Farmbot's internal
network representation into either a VintageNetEthernet
or VintageNetWiFi RawConfig.
"""
@ -71,9 +71,8 @@ defmodule FarmbotOS.Platform.Target.Configurator.Validator do
defp to_ipv4(%{
ipv4_method: "static",
# TODO(Connor) fix nameservers
# name_servers: name_servers,
# domain: domain,
name_servers: name_servers,
domain: domain,
ipv4_address: ipv4_address,
ipv4_gateway: ipv4_gateway,
ipv4_subnet_mask: ipv4_subnet_mask
@ -82,7 +81,9 @@ defmodule FarmbotOS.Platform.Target.Configurator.Validator do
method: :static,
address: ipv4_address,
netmask: ipv4_subnet_mask,
gateway: ipv4_gateway
gateway: ipv4_gateway,
name_servers: name_servers,
domain: domain
}
end

View File

@ -71,7 +71,7 @@ defmodule FarmbotOS.Platform.Target.Network do
_ = maybe_hack_tzdata()
_ = init_net_kernel()
send(self(), :setup)
# If a secret exists, assume that
# If a secret exists, assume that
# farmbot at one point has been connected to the internet
first_connect? = is_first_connect?()
@ -135,6 +135,7 @@ defmodule FarmbotOS.Platform.Target.Network do
end
vintage_net_config = to_vintage_net(config)
FarmbotCore.Logger.info(3, inspect(vintage_net_config))
FarmbotTelemetry.event(:network, :interface_configure, nil,
interface: ifname
@ -260,7 +261,7 @@ defmodule FarmbotOS.Platform.Target.Network do
""")
FarmbotOS.System.factory_reset("""
Farmbot was unable to assosiate with the EAP network.
Farmbot was unable to assosiate with the EAP network.
Please check the identity, password and method of connection
""")
@ -270,11 +271,11 @@ defmodule FarmbotOS.Platform.Target.Network do
def handle_info({VintageNet, property, old, new, _meta}, state) do
Logger.debug("""
Unknown property change: #{inspect(property)}
old:
old:
#{inspect(old, limit: :infinity)}
new:
new:
#{inspect(new, limit: :infinity)}
""")
@ -284,12 +285,12 @@ defmodule FarmbotOS.Platform.Target.Network do
def handle_info({:network_not_found_timer, minutes}, state) do
FarmbotCore.Logger.warn(1, """
Farmbot has been disconnected from the network for
Farmbot has been disconnected from the network for
#{minutes} minutes. Going down for factory reset.
""")
FarmbotOS.System.factory_reset("""
Farmbot has been disconnected from the network for
Farmbot has been disconnected from the network for
#{minutes} minutes.
""")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@
<label for="ssh_key"> id_rsa.pub </label>
<input type="text" id="ssh_key" name="ssh_key" value="">
<label for="dns_name"> DNS test name </label>
<label for="dns_name"> Domain for verifying DNS (eg: "my.farm.bot")</label>
<input type="text" id="dns_name" name="dns_name" value="">
<label for="ntp_server_1"> NTP Pool Server 1 </label>

View File

@ -5,10 +5,23 @@ defmodule FarmbotOs.AvrdudeTest do
setup :verify_on_exit!
test "failure handling" do
reset_fn = fn ->
raise RuntimeError, "foo"
end
expect(FarmbotCore.LogExecutor, :execute, 1, fn log ->
msg = log.message
assert String.contains?(msg, "%RuntimeError{message: \"foo\"}")
end)
Avrdude.call_reset_fun(reset_fn)
end
test "works" do
File.touch("/tmp/wow")
expect(Avrdude.MuonTrapAdapter, :cmd, fn cmd, args, opts ->
expect(MuonTrap, :cmd, 1, fn cmd, args, opts ->
assert cmd == "avrdude"
assert args == [
@ -35,4 +48,35 @@ defmodule FarmbotOs.AvrdudeTest do
"YOLO"
end)
end
test "handles /dev file names, also" do
File.touch("/tmp/wow")
expect(MuonTrap, :cmd, 1, fn cmd, args, opts ->
assert cmd == "avrdude"
assert args == [
"-patmega2560",
"-cwiring",
"-P/dev/null",
"-b115200",
"-D",
"-V",
"-Uflash:w:/tmp/wow:i"
]
assert opts == [
into: %IO.Stream{
device: :standard_io,
line_or_bytes: :line,
raw: false
},
stderr_to_stdout: true
]
end)
Avrdude.flash("/tmp/wow", "/dev/null", fn ->
"YOLO"
end)
end
end

View File

@ -0,0 +1,105 @@
defmodule FarmbotOS.Configurator.ConfigDataLayerTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
alias FarmbotOS.Configurator.ConfigDataLayer
@fake_params %{
"auth_config_email" => System.get_env("FARMBOT_EMAIL", "test@test.com"),
"auth_config_password" => System.get_env("FARMBOT_PASSWORD", "password123"),
"auth_config_server" =>
System.get_env("FARMBOT_SERVER", "http://localhost:3000"),
"ifname" => "eth0",
"iftype" => "wired",
"net_config_dns_name" => nil,
"net_config_domain" => nil,
"net_config_identity" => nil,
"net_config_ipv4_address" => "0.0.0.0",
"net_config_ipv4_gateway" => "0.0.0.0",
"net_config_ipv4_method" => "dhcp",
"net_config_ipv4_subnet_mask" => "255.255.0.0",
"net_config_name_servers" => nil,
"net_config_ntp1" => nil,
"net_config_ntp2" => nil,
"net_config_password" => nil,
"net_config_psk" => nil,
"net_config_reg_domain" => "US",
"net_config_security" => nil,
"net_config_ssh_key" => nil,
"net_config_ssid" => nil
}
test "failure: load_last_reset_reason" do
expect(File, :read, 1, fn _ -> nil end)
assert nil == ConfigDataLayer.load_last_reset_reason()
end
test "success: load_last_reset_reason" do
expect(File, :read, 1, fn _ -> {:ok, "testcase123"} end)
assert "testcase123" == ConfigDataLayer.load_last_reset_reason()
end
test "load_(server|email|password)()" do
:ok = ConfigDataLayer.save_config(@fake_params)
assert @fake_params["auth_config_server"] == ConfigDataLayer.load_server()
assert @fake_params["auth_config_password"] ==
ConfigDataLayer.load_password()
assert @fake_params["auth_config_email"] == ConfigDataLayer.load_email()
end
test "works" do
expected = %{
domain: nil,
identity: nil,
ipv4_address: "0.0.0.0",
ipv4_gateway: "0.0.0.0",
ipv4_method: "dhcp",
ipv4_subnet_mask: "255.255.0.0",
name: "eth0",
name_servers: nil,
password: nil,
psk: nil,
regulatory_domain: "US",
security: nil,
ssid: nil,
type: "wired"
}
FarmbotCore.Config
|> expect(:input_network_config!, 1, fn network_params ->
assert expected == network_params
end)
|> expect(:update_config_value, 7, fn
:string, "authorization", "email", email ->
assert email == @fake_params["auth_config_email"]
:ok
:string, "authorization", "password", pass ->
assert pass == @fake_params["auth_config_password"]
:ok
:string, "authorization", "server", server ->
assert server == @fake_params["auth_config_server"]
:ok
:string, "settings", "default_dns_name", nil ->
:ok
:string, "settings", "default_ntp_server_1", nil ->
:ok
:string, "settings", "default_ntp_server_2", nil ->
:ok
:string, "authorization", "secret", nil ->
:ok
_, _, _, _ ->
raise "NEVER"
end)
assert :ok == ConfigDataLayer.save_config(@fake_params)
end
end

View File

@ -0,0 +1,14 @@
defmodule FarmbotOS.Configurator.LoggerSocketTest do
use ExUnit.Case
use Mimic
alias FarmbotOS.Configurator.LoggerSocket
setup :verify_on_exit!
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}
assert expected == LoggerSocket.init(:foo, :bar)
end
end

View File

@ -1,5 +1,7 @@
defmodule FarmbotOS.Configurator.RouterTest do
alias FarmbotOS.Configurator.Router
alias FarmbotOS.Configurator.ConfigDataLayer
use ExUnit.Case, async: true
use Plug.Test
@ -7,6 +9,30 @@ defmodule FarmbotOS.Configurator.RouterTest do
setup :verify_on_exit!
@opts Router.init([])
# 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(%Plug.Conn{state: :unset}, _status) do
raise "expected connection to have redirected but no response was set/sent"
end
defp redirected_to(conn, status) when is_atom(status) do
redirected_to(conn, Plug.Conn.Status.code(status))
end
defp redirected_to(%Plug.Conn{status: status} = conn, status) do
location = Plug.Conn.get_resp_header(conn, "location") |> List.first()
location || raise "no location header was set on redirected_to"
end
defp redirected_to(conn, status) do
raise "expected redirection with status #{status}, got: #{conn.status}"
end
defp get_con(path) do
conn = conn(:get, path)
Router.call(conn, @opts)
end
test "index after reset" do
FarmbotOS.Configurator.ConfigDataLayer
@ -19,19 +45,25 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.resp_body =~ "whoops!"
end
test "redirect to index" do
FarmbotOS.Configurator.ConfigDataLayer
|> expect(:load_last_reset_reason, fn -> nil end)
test "redirects" do
redirects = [
"/check_network_status.txt",
"/connecttest.txt",
"/gen_204",
"/generate_204",
"/hotspot-detect.html",
"/library/test/success.html",
"/redirect",
"/setup",
"/success.txt"
]
conn = conn(:get, "/setup")
conn = Router.call(conn, @opts)
redir = redirected_to(conn)
assert redir == "/"
conn = conn(:get, redir)
conn = Router.call(conn, @opts)
redir = redirected_to(conn)
assert redir == "/network"
Enum.map(redirects, fn path ->
conn = conn(:get, path)
conn = Router.call(conn, @opts)
redir = redirected_to(conn)
assert redir == "/"
end)
end
test "celeryscript requests don't get listed as last reset reason" do
@ -336,23 +368,60 @@ defmodule FarmbotOS.Configurator.RouterTest do
assert conn.status == 500
end
# 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(%Plug.Conn{state: :unset}, _status) do
raise "expected connection to have redirected but no response was set/sent"
test "/scheduler_debugger" do
kon = get_con("/scheduler_debugger")
assert String.contains?(kon.resp_body, "scheduler_debugger.js")
assert String.contains?(kon.resp_body, "<title>Scheduler Debugger</title>")
end
defp redirected_to(conn, status) when is_atom(status) do
redirected_to(conn, Plug.Conn.Status.code(status))
test "/logger" do
kon = get_con("/logger")
words = [
"DateTime",
"Function",
"Level",
"Message",
"Module",
"Src"
]
Enum.map(words, fn word ->
assert String.contains?(kon.resp_body, word)
end)
end
defp redirected_to(%Plug.Conn{status: status} = conn, status) do
location = Plug.Conn.get_resp_header(conn, "location") |> List.first()
location || raise "no location header was set on redirected_to"
test "/api/telemetry/cpu_usage" do
{:ok, json} = Jason.decode(get_con("/api/telemetry/cpu_usage").resp_body)
assert Enum.count(json) == 10
zero = Enum.at(json, 0)
assert(is_binary(zero["class"]))
assert(is_binary(zero["timestamp"]))
assert(is_integer(zero["value"]))
end
defp redirected_to(conn, status) do
raise "expected redirection with status #{status}, got: #{conn.status}"
test "/finish" do
expect(ConfigDataLayer, :save_config, 1, fn _conf ->
:ok
end)
# This data would crash in the real app because it is incomplete.
# Maybe we should add an error handler?
fake_session = %{
"ifname" => "MY_IFNAME",
"auth_config_email" => "MY_EMAIL",
"auth_config_password" => "MY_PASS",
"auth_config_server" => "MY_SERVER"
}
kon =
conn(:get, "/finish")
|> init_test_session(fake_session)
|> Router.call(@opts)
assert String.contains?(
kon.resp_body,
"If any configuration settings are incorrect, FarmBot will reset"
)
end
end

View File

@ -0,0 +1,148 @@
defmodule FarmbotOS.FarmbotOS.Lua.Ext.DataManipulationTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
def lua(test_name, lua_code) do
FarmbotOS.Lua.eval_assertion(test_name, lua_code)
end
test "update_device()" do
expect(FarmbotCore.Asset, :update_device!, 1, fn params ->
assert %{"name" => "Test Farmbot"} == params
end)
lua_code = """
update_device({name = "Test Farmbot"})
return true
"""
assert true == lua("update device test", lua_code)
end
test "get_device/0" do
fake_device = %{fake: :device}
expect(FarmbotCore.Asset, :device, 1, fn -> fake_device end)
expect(FarmbotCore.Asset.Device, :render, 1, fn dev -> dev end)
lua_code = """
get_device()
return true
"""
assert true == lua("get device test", lua_code)
end
test "get_device/1" do
fake_device = %{name: "my farmbot", id: 23}
expect(FarmbotCore.Asset, :device, 1, fn -> fake_device end)
expect(FarmbotCore.Asset.Device, :render, 1, fn dev -> dev end)
lua_code = """
return get_device("id") == 23
"""
assert true == lua("get device test/1", lua_code)
end
test "update_fbos_config" do
expect(FarmbotCore.Asset, :update_fbos_config!, 1, fn params ->
assert params == %{"foo" => "bar"}
end)
lua_code = """
update_fbos_config({foo = "bar"})
return true
"""
assert true == lua("update_fbos_config test", lua_code)
end
test "get_fbos_config/1" do
fake_config = %{id: 47}
expect(FarmbotCore.Asset, :fbos_config, 1, fn -> fake_config end)
expect(FarmbotCore.Asset.FbosConfig, :render, 1, fn params -> params end)
lua_code = "return 47 == get_fbos_config(\"id\")"
assert true == lua("get_fbos_config", lua_code)
end
test "get_fbos_config/0" do
fake_config = %{id: 47, foo: "bar"}
expect(FarmbotCore.Asset, :fbos_config, 1, fn -> fake_config end)
expect(FarmbotCore.Asset.FbosConfig, :render, 1, fn params -> params end)
lua_code = """
c = get_fbos_config()
return (c.id == 47) and (c.foo == "bar")
"""
assert true == lua("get_fbos_config/1", lua_code)
end
test "update_firmware_config" do
expect(FarmbotCore.Asset, :update_firmware_config!, 1, fn params ->
assert params == %{"foo" => "bar"}
end)
lua_code = """
update_firmware_config({foo = "bar"})
return true
"""
assert true == lua("update_firmware_config test", lua_code)
end
test "get_firmware_config/1" do
fake_config = %{id: 47}
expect(FarmbotCore.Asset, :firmware_config, 1, fn -> fake_config end)
expect(FarmbotCore.Asset.FirmwareConfig, :render, 1, fn params -> params end)
lua_code = "return 47 == get_firmware_config(\"id\")"
assert true == lua("get_firmware_config", lua_code)
end
test "get_firmware_config/0" do
fake_config = %{id: 47, foo: "bar"}
expect(FarmbotCore.Asset, :firmware_config, 1, fn -> fake_config end)
expect(FarmbotCore.Asset.FirmwareConfig, :render, 1, fn params -> params end)
lua_code = """
c = get_firmware_config()
return (c.id == 47) and (c.foo == "bar")
"""
assert true == lua("get_firmware_config/1", lua_code)
end
test "new_farmware_env" do
expect(FarmbotCore.Asset, :new_farmware_env, 1, fn params -> params end)
lua_code = """
return new_farmware_env({foo = "bar"})
"""
assert true == lua("new_farmware_env/1", lua_code)
end
test "new_sensor_reading" do
expect(FarmbotCore.Asset, :new_sensor_reading!, 1, fn params -> params end)
lua_code = """
return new_sensor_reading({
pin = 0,
mode = 1,
value = 2,
x = 3,
y = 4,
z = 5,
})
"""
assert true == lua("new_farmware_env/1", lua_code)
end
end

View File

@ -0,0 +1,28 @@
defmodule FarmbotOS.LuaTest do
use ExUnit.Case
use Mimic
setup :verify_on_exit!
alias FarmbotOS.Lua
test "evaluates Lua" do
assert Lua.eval_assertion("Returns 'true'", "return true")
{:error, message1} = Lua.eval_assertion("Returns 'true'", "-1")
assert message1 ==
"failed to parse expression (line:1): syntax error before: '-'"
{:error, error} = Lua.eval_assertion("random error", "return (1/0)")
assert error == :badarith
end
test "assertion logs" do
# Hmmm
expect(FarmbotCore.LogExecutor, :execute, 1, fn log ->
assert log.level == "assertion"
assert log.message == "this is an assertion"
assert log.meta == %{assertion_passed: true, assertion_type: :assertion}
end)
Lua.log_assertion(true, :assertion, "this is an assertion")
end
end

View File

@ -0,0 +1,24 @@
defmodule FarmbotOS.SysCalls.FarmwareTest do
use ExUnit.Case, async: true
use Mimic
setup :verify_on_exit!
test "farmware_timeout" do
fake_pid = :FAKE_PID
expect(FarmbotCore.LogExecutor, :execute, fn log ->
expected =
"Farmware did not exit after 30.0 seconds. Terminating :FAKE_PID"
assert log.message == expected
:ok
end)
expect(FarmbotCore.FarmwareRuntime, :stop, fn pid ->
assert pid == fake_pid
end)
assert :ok == FarmbotOS.SysCalls.Farmware.farmware_timeout(fake_pid)
end
end

View File

@ -1,8 +0,0 @@
defmodule Avrdude.MuonTrapTestAdapter do
@behaviour Avrdude.MuonTrapAdapter
@impl Avrdude.MuonTrapAdapter
def cmd(exe, args, options) do
{exe, args, options}
end
end

View File

@ -1,10 +1,17 @@
Application.ensure_all_started(:mimic)
Mimic.copy(FarmbotCore.Asset)
Mimic.copy(FarmbotCore.Asset.Device)
Mimic.copy(FarmbotCore.Asset.FbosConfig)
Mimic.copy(FarmbotCore.Asset.FirmwareConfig)
Mimic.copy(FarmbotCore.Config)
Mimic.copy(FarmbotCore.FarmwareRuntime)
Mimic.copy(FarmbotCore.LogExecutor)
Mimic.copy(FarmbotExt.API.Reconciler)
Mimic.copy(FarmbotExt.API)
Mimic.copy(FarmbotCore.Asset)
Mimic.copy(FarmbotFirmware)
Mimic.copy(FarmbotOS.Configurator.ConfigDataLayer)
Mimic.copy(FarmbotOS.Configurator.FakeNetworkLayer)
Mimic.copy(FarmbotOS.Configurator.DetsTelemetryLayer)
Mimic.copy(Avrdude.MuonTrapAdapter)
Mimic.copy(FarmbotOS.Configurator.FakeNetworkLayer)
Mimic.copy(File)
Mimic.copy(MuonTrap)
ExUnit.start()

View File

@ -1,15 +1 @@
defmodule Farmbot.TestSupport do
alias FarmbotCore.AssetWorker.FarmbotCore.Asset.{FarmEvent, RegimenInstance}
def farm_event_timeout do
Application.get_env(:farmbot_core, FarmEvent)[:checkup_time_ms]
end
def regimen_instance_timeout do
Application.get_env(:farmbot_core, RegimenInstance)[:checkup_time_ms]
end
def asset_monitor_timeout do
Application.get_env(:farmbot_core, Farmbot.AssetMonitor)[:checkup_time_ms]
end
end