Merge branch 'staging' into issue-1685

pull/1726/head
Fabio Dessi 2020-03-03 09:02:11 +01:00
commit dd46830b9d
692 changed files with 7516 additions and 5415 deletions

View File

@ -24,7 +24,7 @@ gem "scenic"
gem "secure_headers" gem "secure_headers"
gem "tzinfo" # For validation of user selected timezone names gem "tzinfo" # For validation of user selected timezone names
gem "valid_url" gem "valid_url"
# gem "farady", "~> 1.0.0" gem "kaminari"
group :development, :test do group :development, :test do
gem "climate_control" gem "climate_control"

View File

@ -72,7 +72,7 @@ GEM
amq-protocol (2.3.0) amq-protocol (2.3.0)
bcrypt (3.1.13) bcrypt (3.1.13)
builder (3.2.4) builder (3.2.4)
bunny (2.14.3) bunny (2.14.4)
amq-protocol (~> 2.3, >= 2.3.0) amq-protocol (~> 2.3, >= 2.3.0)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
@ -82,9 +82,9 @@ GEM
simplecov simplecov
url url
coderay (1.1.2) coderay (1.1.2)
concurrent-ruby (1.1.5) concurrent-ruby (1.1.6)
crass (1.0.6) crass (1.0.6)
database_cleaner (1.7.0) database_cleaner (1.8.3)
declarative (0.0.10) declarative (0.0.10)
declarative-option (0.1.0) declarative-option (0.1.0)
delayed_job (4.1.8) delayed_job (4.1.8)
@ -100,7 +100,7 @@ GEM
warden (~> 1.2.3) warden (~> 1.2.3)
diff-lcs (1.3) diff-lcs (1.3)
digest-crc (0.4.1) digest-crc (0.4.1)
discard (1.1.0) discard (1.2.0)
activerecord (>= 4.2, < 7) activerecord (>= 4.2, < 7)
docile (1.3.2) docile (1.3.2)
erubi (1.9.0) erubi (1.9.0)
@ -109,7 +109,7 @@ GEM
factory_bot_rails (5.1.1) factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0) factory_bot (~> 5.1.0)
railties (>= 4.2.0) railties (>= 4.2.0)
faker (2.10.1) faker (2.10.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (0.15.4) faraday (0.15.4)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@ -119,7 +119,7 @@ GEM
railties (>= 3.2, < 6.1) railties (>= 3.2, < 6.1)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
google-api-client (0.36.4) google-api-client (0.37.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9) googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0) httpclient (>= 2.8.1, < 3.0)
@ -127,10 +127,12 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
signet (~> 0.12) signet (~> 0.12)
google-cloud-core (1.4.1) google-cloud-core (1.5.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0) google-cloud-env (1.3.0)
faraday (~> 0.11) faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1) google-cloud-storage (1.25.1)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
@ -153,6 +155,18 @@ GEM
json (2.3.0) json (2.3.0)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.2.1) jwt (2.2.1)
kaminari (1.2.0)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.0)
kaminari-activerecord (= 1.2.0)
kaminari-core (= 1.2.0)
kaminari-actionview (1.2.0)
actionview
kaminari-core (= 1.2.0)
kaminari-activerecord (1.2.0)
activerecord
kaminari-core (= 1.2.0)
kaminari-core (1.2.0)
loofah (2.4.0) loofah (2.4.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
@ -162,7 +176,7 @@ GEM
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
memoist (0.16.2) memoist (0.16.2)
method_source (0.9.2) method_source (0.9.2)
mimemagic (0.3.3) mimemagic (0.3.4)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.14.0) minitest (5.14.0)
@ -171,7 +185,7 @@ GEM
mutations (0.9.0) mutations (0.9.0)
activesupport activesupport
nio4r (2.5.2) nio4r (2.5.2)
nokogiri (1.10.7) nokogiri (1.10.8)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.0.1) os (1.0.1)
@ -190,7 +204,7 @@ GEM
faraday_middleware (~> 0.13.0) faraday_middleware (~> 0.13.0)
hashie (~> 3.6) hashie (~> 3.6)
multi_json (~> 1.13.1) multi_json (~> 1.13.1)
rack (2.1.1) rack (2.2.2)
rack-attack (6.2.2) rack-attack (6.2.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
@ -240,7 +254,7 @@ GEM
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
retriable (3.1.2) retriable (3.1.2)
rollbar (2.23.2) rollbar (2.24.0)
rspec (3.9.0) rspec (3.9.0)
rspec-core (~> 3.9.0) rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0) rspec-expectations (~> 3.9.0)
@ -264,7 +278,7 @@ GEM
rspec-support (3.9.2) rspec-support (3.9.2)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
scenic (1.5.1) scenic (1.5.2)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
secure_headers (6.3.0) secure_headers (6.3.0)
@ -273,11 +287,10 @@ GEM
faraday (~> 0.9) faraday (~> 0.9)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simplecov (0.17.1) simplecov (0.18.5)
docile (~> 1.1) docile (~> 1.1)
json (>= 1.8, < 3) simplecov-html (~> 0.11)
simplecov-html (~> 0.10.0) simplecov-html (0.12.1)
simplecov-html (0.10.2)
sprockets (4.0.0) sprockets (4.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
@ -320,6 +333,7 @@ DEPENDENCIES
google-cloud-storage (~> 1.11) google-cloud-storage (~> 1.11)
hashdiff hashdiff
jwt jwt
kaminari
mutations mutations
passenger passenger
pg pg

View File

@ -80,6 +80,16 @@ module Api
{ root: false, user: current_user } { root: false, user: current_user }
end end
def maybe_paginate(collection)
page = params[:page]
per = params[:per]
if page && per
render json: collection.page(page).per(per)
else
render json: collection
end
end
private private
def clean_expired_farm_events def clean_expired_farm_events

View File

@ -1,7 +1,7 @@
module Api module Api
class AlertsController < Api::AbstractController class AlertsController < Api::AbstractController
def index def index
render json: current_device.alerts maybe_paginate current_device.alerts
end end
def destroy def destroy

View File

@ -3,7 +3,7 @@ module Api
before_action :clean_expired_farm_events, only: [:index] before_action :clean_expired_farm_events, only: [:index]
def index def index
render json: current_device.farm_events maybe_paginate current_device.farm_events
end end
def show def show

View File

@ -10,7 +10,7 @@ module Api
end end
def index def index
render json: farmware_envs maybe_paginate farmware_envs
end end
def show def show

View File

@ -1,7 +1,7 @@
module Api module Api
class FarmwareInstallationsController < Api::AbstractController class FarmwareInstallationsController < Api::AbstractController
def index def index
render json: farmware_installations maybe_paginate farmware_installations
end end
def show def show

View File

@ -1,7 +1,7 @@
module Api module Api
class PeripheralsController < Api::AbstractController class PeripheralsController < Api::AbstractController
def index def index
render json: current_device.peripherals maybe_paginate current_device.peripherals
end end
def show def show

View File

@ -1,7 +1,7 @@
module Api module Api
class PinBindingsController < Api::AbstractController class PinBindingsController < Api::AbstractController
def index def index
render json: pin_bindings maybe_paginate pin_bindings
end end
def show def show

View File

@ -1,7 +1,7 @@
module Api module Api
class PlantTemplatesController < Api::AbstractController class PlantTemplatesController < Api::AbstractController
def index def index
render json: current_device.plant_templates maybe_paginate current_device.plant_templates
end end
def create def create

View File

@ -3,7 +3,7 @@ module Api
before_action :clean_expired_farm_events, only: [:destroy] before_action :clean_expired_farm_events, only: [:destroy]
def index def index
render json: your_point_groups maybe_paginate your_point_groups
end end
def show def show

View File

@ -20,7 +20,7 @@ module Api
.where("discarded_at < ?", Time.now - HARD_DELETE_AFTER) .where("discarded_at < ?", Time.now - HARD_DELETE_AFTER)
.destroy_all .destroy_all
render json: points(params.fetch(:filter) { "kept" }) maybe_paginate points(params.fetch(:filter) { "kept" })
end end
def show def show

View File

@ -3,7 +3,7 @@ module Api
before_action :clean_expired_farm_events, only: [:destroy] before_action :clean_expired_farm_events, only: [:destroy]
def index def index
render json: your_regimens maybe_paginate your_regimens
end end
def show def show

View File

@ -1,7 +1,7 @@
module Api module Api
class SavedGardensController < Api::AbstractController class SavedGardensController < Api::AbstractController
def index def index
render json: current_device.saved_gardens maybe_paginate current_device.saved_gardens
end end
def create def create

View File

@ -1,11 +1,14 @@
module Api module Api
class SensorReadingsController < Api::AbstractController class SensorReadingsController < Api::AbstractController
LIMIT = 5000
before_action :clean_old
def create def create
mutate SensorReadings::Create.run(raw_json, device: current_device) mutate SensorReadings::Create.run(raw_json, device: current_device)
end end
def index def index
render json: readings maybe_paginate(readings)
end end
def show def show
@ -17,10 +20,23 @@ module Api
render json: "" render json: ""
end end
private private
def clean_old
if current_device.sensor_readings.count > LIMIT
current_device
.sensor_readings
.where
.not(id: readings.pluck(:id))
.delete_all
end
end
def readings def readings
SensorReading.where(device: current_device) @readings ||= SensorReading
.where(device: current_device)
.order(created_at: :desc)
.limit(LIMIT)
end end
def reading def reading

View File

@ -1,7 +1,7 @@
module Api module Api
class SensorsController < Api::AbstractController class SensorsController < Api::AbstractController
def index def index
render json: current_device.sensors maybe_paginate current_device.sensors
end end
def show def show

View File

@ -2,7 +2,7 @@ module Api
class ToolsController < Api::AbstractController class ToolsController < Api::AbstractController
def index def index
render json: tools maybe_paginate tools
end end
def show def show

View File

@ -7,7 +7,7 @@ module Api
end end
def index def index
render json: webcams maybe_paginate webcams
end end
def show def show

View File

@ -171,9 +171,10 @@ class Device < ApplicationRecord
end end
TOO_MANY_CONNECTIONS = TOO_MANY_CONNECTIONS =
"Your device is " + "Your device is reconnecting to the server too often. " +
"reconnecting to the server too often. Please " + "This may be a sign of local network issues. " +
"see https://developer.farm.bot/docs/connectivity-issues" "Please review the documentation provided at " +
"https://software.farm.bot/docs/connecting-farmbot-to-the-internet"
def self.connection_warning(username) def self.connection_warning(username)
device_id = username.split("_").last.to_i || 0 device_id = username.split("_").last.to_i || 0
device = self.find_by(id: device_id) device = self.find_by(id: device_id)

View File

@ -6,7 +6,7 @@ class InUsePoint < ApplicationRecord
DEFAULT_NAME = "point" DEFAULT_NAME = "point"
FANCY_NAMES = { FANCY_NAMES = {
GenericPointer.name => DEFAULT_NAME, GenericPointer.name => DEFAULT_NAME,
ToolSlot.name => "tool slot", ToolSlot.name => "slot",
Plant.name => "plant", Plant.name => "plant",
} }

View File

@ -4,7 +4,7 @@ class PointGroup < ApplicationRecord
BAD_SORT = "%{value} is not valid. Valid options are: " + BAD_SORT = "%{value} is not valid. Valid options are: " +
SORT_TYPES.map(&:inspect).join(", ") SORT_TYPES.map(&:inspect).join(", ")
DEFAULT_CRITERIA = { DEFAULT_CRITERIA = {
day: { op: "<", days: 0 }, day: { op: "<", days_ago: 0 },
string_eq: {}, string_eq: {},
number_eq: {}, number_eq: {},
number_lt: {}, number_lt: {},

View File

@ -11,7 +11,7 @@ class ToolSlot < Point
MIN_PULLOUT = PULLOUT_DIRECTIONS.min MIN_PULLOUT = PULLOUT_DIRECTIONS.min
PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\ PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\
"%{value} is not valid." "%{value} is not valid."
IN_USE = "already in use by another tool slot. "\ IN_USE = "already in use by another slot. "\
"Please un-assign the tool from its current slot"\ "Please un-assign the tool from its current slot"\
" before reassigning." " before reassigning."

View File

@ -7,7 +7,9 @@ module Devices
"genesis_1.2" => Devices::Seeders::GenesisOneTwo, "genesis_1.2" => Devices::Seeders::GenesisOneTwo,
"genesis_1.3" => Devices::Seeders::GenesisOneThree, "genesis_1.3" => Devices::Seeders::GenesisOneThree,
"genesis_1.4" => Devices::Seeders::GenesisOneFour, "genesis_1.4" => Devices::Seeders::GenesisOneFour,
"genesis_1.5" => Devices::Seeders::GenesisOneFive,
"genesis_xl_1.4" => Devices::Seeders::GenesisXlOneFour, "genesis_xl_1.4" => Devices::Seeders::GenesisXlOneFour,
"genesis_xl_1.5" => Devices::Seeders::GenesisXlOneFive,
"demo_account" => Devices::Seeders::DemoAccountSeeder, "demo_account" => Devices::Seeders::DemoAccountSeeder,
"none" => Devices::Seeders::None, "none" => Devices::Seeders::None,

View File

@ -27,7 +27,7 @@ module Devices
add_tool_slot(name: ToolNames::SEED_TROUGH_1, add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0, x: 0,
y: 25, y: 25,
z: -200, z: 0,
tool: tools_seed_trough_1, tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE, pullout_direction: ToolSlot::NONE,
gantry_mounted: true) gantry_mounted: true)
@ -37,25 +37,18 @@ module Devices
add_tool_slot(name: ToolNames::SEED_TROUGH_2, add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0, x: 0,
y: 50, y: 50,
z: -200, z: 0,
tool: tools_seed_trough_2, tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE, pullout_direction: ToolSlot::NONE,
gantry_mounted: true) gantry_mounted: true)
end end
def tool_slots_slot_3 def tool_slots_slot_3; end
add_tool_slot(name: ToolNames::SEED_TROUGH_3,
x: 0,
y: 75,
z: -200,
tool: tools_seed_trough_3,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_4; end def tool_slots_slot_4; end
def tool_slots_slot_5; end def tool_slots_slot_5; end
def tool_slots_slot_6; end def tool_slots_slot_6; end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin; end def tools_seed_bin; end
def tools_seed_tray; end def tools_seed_tray; end
@ -69,11 +62,6 @@ module Devices
add_tool(ToolNames::SEED_TROUGH_2) add_tool(ToolNames::SEED_TROUGH_2)
end end
def tools_seed_trough_3
@tools_seed_trough_3 ||=
add_tool(ToolNames::SEED_TROUGH_3)
end
def tools_seeder; end def tools_seeder; end
def tools_soil_sensor; end def tools_soil_sensor; end
def tools_watering_nozzle; end def tools_watering_nozzle; end

View File

@ -75,6 +75,9 @@ module Devices
tool: tools_weeder) tool: tools_weeder)
end end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin def tools_seed_bin
@tools_seed_bin ||= @tools_seed_bin ||=
add_tool(ToolNames::SEED_BIN) add_tool(ToolNames::SEED_BIN)
@ -87,7 +90,6 @@ module Devices
def tools_seed_trough_1; end def tools_seed_trough_1; end
def tools_seed_trough_2; end def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder def tools_seeder
@tools_seeder ||= @tools_seeder ||=

View File

@ -37,7 +37,6 @@ module Devices
:tools_seed_tray, :tools_seed_tray,
:tools_seed_trough_1, :tools_seed_trough_1,
:tools_seed_trough_2, :tools_seed_trough_2,
:tools_seed_trough_3,
:tools_seeder, :tools_seeder,
:tools_soil_sensor, :tools_soil_sensor,
:tools_watering_nozzle, :tools_watering_nozzle,
@ -50,6 +49,8 @@ module Devices
:tool_slots_slot_4, :tool_slots_slot_4,
:tool_slots_slot_5, :tool_slots_slot_5,
:tool_slots_slot_6, :tool_slots_slot_6,
:tool_slots_slot_7,
:tool_slots_slot_8,
# WEBCAM FEEDS =========================== # WEBCAM FEEDS ===========================
:webcam_feeds, :webcam_feeds,
@ -152,11 +153,12 @@ module Devices
def tool_slots_slot_4; end def tool_slots_slot_4; end
def tool_slots_slot_5; end def tool_slots_slot_5; end
def tool_slots_slot_6; end def tool_slots_slot_6; end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin; end def tools_seed_bin; end
def tools_seed_tray; end def tools_seed_tray; end
def tools_seed_trough_1; end def tools_seed_trough_1; end
def tools_seed_trough_2; end def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder; end def tools_seeder; end
def tools_soil_sensor; end def tools_soil_sensor; end
def tools_watering_nozzle; end def tools_watering_nozzle; end

View File

@ -31,7 +31,6 @@ module Devices
LIGHTING = "Lighting" LIGHTING = "Lighting"
SEED_TROUGH_1 = "Seed Trough 1" SEED_TROUGH_1 = "Seed Trough 1"
SEED_TROUGH_2 = "Seed Trough 2" SEED_TROUGH_2 = "Seed Trough 2"
SEED_TROUGH_3 = "Seed Trough 3"
end end
# Stub plants ============================== # Stub plants ==============================

View File

@ -0,0 +1,41 @@
module Devices
module Seeders
class GenesisOneFive < AbstractGenesis
def settings_firmware
device
.fbos_config
.update!(firmware_hardware: FbosConfig::FARMDUINO_K15)
end
def tool_slots_slot_7
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0,
y: 25,
z: 0,
tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_8
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0,
y: 50,
z: 0,
tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tools_seed_trough_1
@tools_seed_trough_1 ||=
add_tool(ToolNames::SEED_TROUGH_1)
end
def tools_seed_trough_2
@tools_seed_trough_2 ||=
add_tool(ToolNames::SEED_TROUGH_2)
end
end
end
end

View File

@ -0,0 +1,53 @@
module Devices
module Seeders
class GenesisXlOneFive < AbstractGenesis
def settings_firmware
device
.fbos_config
.update!(firmware_hardware: FbosConfig::FARMDUINO_K15)
end
def settings_device_name
device.update!(name: "FarmBot Genesis XL")
end
def settings_default_map_size_x
device.web_app_config.update!(map_size_x: 5_900)
end
def settings_default_map_size_y
device.web_app_config.update!(map_size_y: 2_900)
end
def tool_slots_slot_7
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0,
y: 25,
z: 0,
tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_8
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0,
y: 50,
z: 0,
tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tools_seed_trough_1
@tools_seed_trough_1 ||=
add_tool(ToolNames::SEED_TROUGH_1)
end
def tools_seed_trough_2
@tools_seed_trough_2 ||=
add_tool(ToolNames::SEED_TROUGH_2)
end
end
end
end

View File

@ -28,11 +28,12 @@ module Devices
def tool_slots_slot_4; end def tool_slots_slot_4; end
def tool_slots_slot_5; end def tool_slots_slot_5; end
def tool_slots_slot_6; end def tool_slots_slot_6; end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin; end def tools_seed_bin; end
def tools_seed_tray; end def tools_seed_tray; end
def tools_seed_trough_1; end def tools_seed_trough_1; end
def tools_seed_trough_2; end def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder; end def tools_seeder; end
def tools_soil_sensor; end def tools_soil_sensor; end
def tools_watering_nozzle; end def tools_watering_nozzle; end

View File

@ -5,7 +5,7 @@ module PointGroups
hash :criteria do hash :criteria do
hash(:day) do hash(:day) do
string :op, in: [">", "<"] string :op, in: [">", "<"]
integer :days integer :days_ago
end end
hash(:string_eq) { array :*, class: String } hash(:string_eq) { array :*, class: String }
hash(:number_eq) { array :*, class: Integer } hash(:number_eq) { array :*, class: Integer }

View File

@ -1,9 +1,9 @@
module Tools module Tools
class Destroy < Mutations::Command class Destroy < Mutations::Command
STILL_IN_USE = "Can't delete tool because the following sequences are "\ STILL_IN_USE = "Can't delete tool or seed container because the " \
"still using it: %s" "following sequences are still using it: %s"
STILL_IN_SLOT = "Can't delete tool because it is still in a tool slot. "\ STILL_IN_SLOT = "Can't delete tool or seed container because it is " \
"Please remove it from the tool slot first." "still in a slot. Please remove it from the slot first."
required do required do
model :tool, class: Tool model :tool, class: Tool
@ -15,10 +15,11 @@ module Tools
end end
def execute def execute
maybe_unmount_tool
tool.destroy! tool.destroy!
end end
private private
def slot def slot
@slot ||= tool.tool_slot @slot ||= tool.tool_slot
@ -33,8 +34,14 @@ private
end end
def names def names
@names ||= \ @names ||=
InUseTool.where(tool_id: tool.id).pluck(:sequence_name).join(", ") InUseTool.where(tool_id: tool.id).pluck(:sequence_name).join(", ")
end end
def maybe_unmount_tool
if tool.device.mounted_tool_id == tool.id
tool.device.update!(mounted_tool_id: nil)
end
end
end end
end end

View File

@ -4,4 +4,8 @@ class PointGroupSerializer < ApplicationSerializer
def point_ids def point_ids
object.point_group_items.pluck(:point_id) object.point_group_items.pluck(:point_id)
end end
def criteria
object.criteria || PointGroup::DEFAULT_CRITERIA
end
end end

View File

@ -4,9 +4,15 @@ export const panelState = (): ControlPanelState => {
return { return {
homing_and_calibration: false, homing_and_calibration: false,
motors: false, motors: false,
encoders_and_endstops: false, encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: false, danger_zone: false,
power_and_reset: false, power_and_reset: false,
pin_guard: false pin_guard: false,
farm_designer: false,
firmware: false,
farmbot_os: false,
}; };
}; };

View File

@ -1,16 +1,10 @@
import { Everything } from "../../interfaces"; import { Everything } from "../../interfaces";
import { panelState } from "../control_panel_state";
export const bot: Everything["bot"] = { export const bot: Everything["bot"] = {
"consistent": true, "consistent": true,
"stepSize": 100, "stepSize": 100,
"controlPanelState": { "controlPanelState": panelState(),
"homing_and_calibration": false,
"motors": false,
"encoders_and_endstops": false,
"danger_zone": false,
"power_and_reset": false,
"pin_guard": false,
},
"hardware": { "hardware": {
"gpio_registry": {}, "gpio_registry": {},
"mcu_params": { "mcu_params": {

View File

@ -54,5 +54,5 @@ export const fakeImages: TaggedImage[] = [
} }
}, },
"uuid": "Image.7.5" "uuid": "Image.7.5"
} },
]; ];

View File

@ -29,7 +29,7 @@ import {
} from "farmbot"; } from "farmbot";
import { fakeResource } from "../fake_resource"; import { fakeResource } from "../fake_resource";
import { import {
ExecutableType, PinBindingType, Folder ExecutableType, PinBindingType, Folder,
} from "farmbot/dist/resources/api_resources"; } from "farmbot/dist/resources/api_resources";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { MessageType } from "../../sequences/interfaces"; import { MessageType } from "../../sequences/interfaces";
@ -460,7 +460,7 @@ export function fakePointGroup(): TaggedPointGroup {
sort_type: "xy_ascending", sort_type: "xy_ascending",
point_ids: [], point_ids: [],
criteria: { criteria: {
day: { op: "<", days: 0 }, day: { op: "<", days_ago: 0 },
number_eq: {}, number_eq: {},
number_gt: {}, number_gt: {},
number_lt: {}, number_lt: {},

View File

@ -2,7 +2,7 @@ import { Coordinate } from "farmbot";
import { VariableNameSet } from "../resources/interfaces"; import { VariableNameSet } from "../resources/interfaces";
export const fakeVariableNameSet = ( export const fakeVariableNameSet = (
label = "parent", vector = { x: 0, y: 0, z: 0 } label = "parent", vector = { x: 0, y: 0, z: 0 },
): VariableNameSet => { ): VariableNameSet => {
const data_value: Coordinate = { const data_value: Coordinate = {
kind: "coordinate", args: vector kind: "coordinate", args: vector

View File

@ -1,6 +1,6 @@
import moment from "moment"; import moment from "moment";
import { import {
FarmEventWithExecutable FarmEventWithExecutable,
} from "../farm_designer/farm_events/calendar/interfaces"; } from "../farm_designer/farm_events/calendar/interfaces";
export const TIME = { export const TIME = {
@ -24,7 +24,7 @@ export const fakeFarmEventWithExecutable = (): FarmEventWithExecutable => {
color: "red", color: "red",
name: "faker", name: "faker",
kind: "sequence", kind: "sequence",
args: { version: 0, locals: { kind: "scope_declaration", args: {} }, } args: { version: 0, locals: { kind: "scope_declaration", args: {} } }
} }
}; };
}; };
@ -84,7 +84,7 @@ export const calendarRows = [
"subheading": "25", "subheading": "25",
"id": 79, "id": 79,
"childExecutableName": "Goto 0, 0, 0 123" "childExecutableName": "Goto 0, 0, 0 123"
} },
] ]
}, },
{ {
@ -171,7 +171,7 @@ export const calendarRows = [
"subheading": "25", "subheading": "25",
"id": 79, "id": 79,
"childExecutableName": "Goto 0, 0, 0 123" "childExecutableName": "Goto 0, 0, 0 123"
} },
] ]
}, },
{ {
@ -258,7 +258,7 @@ export const calendarRows = [
"subheading": "25", "subheading": "25",
"id": 79, "id": 79,
"childExecutableName": "Goto 0, 0, 0 123" "childExecutableName": "Goto 0, 0, 0 123"
} },
] ]
} },
]; ];

View File

@ -62,7 +62,7 @@ const tr0: TaggedResource = {
}, },
"speed": 100 "speed": 100
} }
} },
], ],
"args": { "args": {
"version": 4, "version": 4,
@ -287,7 +287,7 @@ const tr12: TaggedResource = {
"regimen_id": 11, "regimen_id": 11,
"sequence_id": 23, "sequence_id": 23,
"time_offset": 345900000 "time_offset": 345900000
} },
], ],
body: [], body: [],
}, },
@ -345,7 +345,7 @@ export const FAKE_RESOURCES: TaggedResource[] = [
tr0, tr0,
tr14, tr14,
tr15, tr15,
log log,
]; ];
const KIND: keyof TaggedResource = "kind"; // Safety first, kids. const KIND: keyof TaggedResource = "kind"; // Safety first, kids.
type ResourceGroupNumber = 0 | 1 | 2 | 3 | 4; type ResourceGroupNumber = 0 | 1 | 2 | 3 | 4;

View File

@ -9,11 +9,11 @@ import { RawApp as App, AppProps, mapStateToProps } from "../app";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { bot } from "../__test_support__/fake_state/bot"; import { bot } from "../__test_support__/fake_state/bot";
import { import {
fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv,
} from "../__test_support__/fake_state/resources"; } from "../__test_support__/fake_state/resources";
import { fakeState } from "../__test_support__/fake_state"; import { fakeState } from "../__test_support__/fake_state";
import { import {
buildResourceIndex buildResourceIndex,
} from "../__test_support__/resource_index_builder"; } from "../__test_support__/resource_index_builder";
import { ResourceName } from "farmbot"; import { ResourceName } from "farmbot";
import { fakeTimeSettings } from "../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../__test_support__/fake_time_settings";
@ -125,7 +125,7 @@ describe("<App />: NavBar", () => {
"Device", "Device",
"Sequences", "Sequences",
"Regimens", "Regimens",
"Farmware" "Farmware",
]; ];
strings.map(string => expect(t).toContain(string)); strings.map(string => expect(t).toContain(string));
wrapper.unmount(); wrapper.unmount();
@ -157,7 +157,6 @@ describe("mapStateToProps()", () => {
const state = fakeState(); const state = fakeState();
const config = fakeFbosConfig(); const config = fakeFbosConfig();
config.body.auto_sync = true; config.body.auto_sync = true;
config.body.api_migrated = true;
const fakeEnv = fakeFarmwareEnv(); const fakeEnv = fakeFarmwareEnv();
state.resources = buildResourceIndex([config, fakeEnv]); state.resources = buildResourceIndex([config, fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" }; state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };

View File

@ -1,17 +1,16 @@
jest.mock("../util", () => { jest.mock("../util", () => ({
return { attachToRoot: jest.fn(),
attachToRoot: jest.fn(), // Incidental mock. Can be removed if errors go away.
// Incidental mock. Can be removed if errors go away. trim: jest.fn(x => x),
trim: jest.fn(x => x) urlFriendly: jest.fn(),
}; }));
});
jest.mock("../redux/store", () => { jest.mock("../redux/store", () => {
return { store: { dispatch: jest.fn() } }; return { store: { dispatch: jest.fn() } };
}); });
jest.mock("../account/dev/dev_support", () => ({ jest.mock("../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => false, } DevSettings: { futureFeaturesEnabled: () => false }
})); }));
jest.mock("../config/actions", () => { jest.mock("../config/actions", () => {

View File

@ -0,0 +1,33 @@
jest.unmock("../external_urls");
import { ExternalUrl } from "../external_urls";
/* tslint:disable:max-line-length */
describe("ExternalUrl", () => {
it("returns urls", () => {
expect(ExternalUrl.featureMinVersions)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/FEATURE_MIN_VERSIONS.json");
expect(ExternalUrl.osReleaseNotes)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md");
expect(ExternalUrl.latestRelease)
.toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest");
expect(ExternalUrl.webAppRepo)
.toEqual("https://github.com/FarmBot/Farmbot-Web-App");
expect(ExternalUrl.gitHubFarmBot)
.toEqual("https://github.com/FarmBot");
expect(ExternalUrl.softwareDocs)
.toEqual("https://software.farm.bot/docs");
expect(ExternalUrl.softwareForum)
.toEqual("https://forum.farmbot.org/c/software");
expect(ExternalUrl.OpenFarm.cropApi)
.toEqual("https://openfarm.cc/api/v1/crops/");
expect(ExternalUrl.OpenFarm.cropBrowse)
.toEqual("https://openfarm.cc/crops/");
expect(ExternalUrl.OpenFarm.newCrop)
.toEqual("https://openfarm.cc/en/crops/new");
expect(ExternalUrl.Video.desktop)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018");
expect(ExternalUrl.Video.mobile)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097");
});
});

View File

@ -19,7 +19,7 @@ jest.mock("../session", () => ({
})); }));
import { import {
responseFulfilled, isLocalRequest, requestFulfilled, responseRejected responseFulfilled, isLocalRequest, requestFulfilled, responseRejected,
} from "../interceptors"; } from "../interceptors";
import { AxiosResponse, Method } from "axios"; import { AxiosResponse, Method } from "axios";
import { uuid } from "farmbot"; import { uuid } from "farmbot";

View File

@ -30,7 +30,6 @@ import "../regimens/editor/interfaces";
import "../regimens/interfaces"; import "../regimens/interfaces";
import "../resources/interfaces"; import "../resources/interfaces";
import "../sequences/interfaces"; import "../sequences/interfaces";
import "../tools/interfaces";
describe("interfaces", () => { describe("interfaces", () => {
it("cant explain why coverage is 0 for interface files", () => { it("cant explain why coverage is 0 for interface files", () => {

View File

@ -9,7 +9,7 @@ jest.mock("axios", () => ({
})); }));
jest.mock("../session", () => ({ Session: { clear: jest.fn(), } })); jest.mock("../session", () => ({ Session: { clear: jest.fn() } }));
import { maybeRefreshToken } from "../refresh_token"; import { maybeRefreshToken } from "../refresh_token";
import { API } from "../api/index"; import { API } from "../api/index";

View File

@ -1,6 +1,6 @@
import { import {
buildResourceIndex, buildResourceIndex,
FAKE_RESOURCES FAKE_RESOURCES,
} from "../__test_support__/resource_index_builder"; } from "../__test_support__/resource_index_builder";
import { TaggedFarmEvent, SpecialStatus } from "farmbot"; import { TaggedFarmEvent, SpecialStatus } from "farmbot";

View File

@ -12,7 +12,7 @@ type Info = UnboundRouteConfig<{}, {}>;
const fakeCallback = ( const fakeCallback = (
component: ConnectedComponent, component: ConnectedComponent,
child: ConnectedComponent | undefined, child: ConnectedComponent | undefined,
info: Info info: Info,
) => { ) => {
if (info.$ == "*") { if (info.$ == "*") {
expect(component.name).toEqual("FourOhFour"); expect(component.name).toEqual("FourOhFour");

View File

@ -11,7 +11,7 @@ jest.mock("axios", () => ({
import { API } from "../../api"; import { API } from "../../api";
import { Content } from "../../constants"; import { Content } from "../../constants";
import { import {
requestAccountExport, generateFilename requestAccountExport, generateFilename,
} from "../request_account_export"; } from "../request_account_export";
import { success } from "../../toast/toast"; import { success } from "../../toast/toast";
import axios from "axios"; import axios from "axios";

View File

@ -3,7 +3,7 @@ import {
Widget, Widget,
WidgetHeader, WidgetHeader,
WidgetBody, WidgetBody,
SaveBtn SaveBtn,
} from "../../ui/index"; } from "../../ui/index";
import { SpecialStatus } from "farmbot"; import { SpecialStatus } from "farmbot";
import Axios from "axios"; import Axios from "axios";

View File

@ -20,7 +20,7 @@ export class DangerousDeleteWidget extends
return <Widget> return <Widget>
<WidgetHeader title={this.props.title} /> <WidgetHeader title={this.props.title} />
<WidgetBody> <WidgetBody>
<div> <div className={"dangerous-delete-warning-messages"}>
{t(this.props.warning)} {t(this.props.warning)}
<br /><br /> <br /><br />
{t(this.props.confirmation)} {t(this.props.confirmation)}
@ -42,6 +42,7 @@ export class DangerousDeleteWidget extends
<button <button
onClick={this.onClick} onClick={this.onClick}
className="red fb-button" className="red fb-button"
title={t(this.props.title)}
type="button"> type="button">
{t(this.props.title)} {t(this.props.title)}
</button> </button>

View File

@ -7,7 +7,7 @@ export function ExportAccountPanel(props: { onClick: () => void }) {
return <Widget> return <Widget>
<WidgetHeader title={t("Export Account Data")} /> <WidgetHeader title={t("Export Account Data")} />
<WidgetBody> <WidgetBody>
<div> <div className={"export-account-data-description"}>
{t(Content.EXPORT_DATA_DESC)} {t(Content.EXPORT_DATA_DESC)}
</div> </div>
<form> <form>
@ -19,6 +19,7 @@ export function ExportAccountPanel(props: { onClick: () => void }) {
</Col> </Col>
<Col xs={4}> <Col xs={4}>
<button className="green fb-button" type="button" <button className="green fb-button" type="button"
title={t("Export")}
onClick={props.onClick}> onClick={props.onClick}>
{t("Export")} {t("Export")}
</button> </button>

View File

@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { import {
BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn,
} from "../../ui/index"; } from "../../ui/index";
import { SettingsPropTypes } from "../interfaces"; import { SettingsPropTypes } from "../interfaces";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";

View File

@ -8,7 +8,7 @@ import { DevMode } from "../dev_mode";
import * as React from "react"; import * as React from "react";
import { range } from "lodash"; import { range } from "lodash";
import { import {
setWebAppConfigValue setWebAppConfigValue,
} from "../../../config_storage/actions"; } from "../../../config_storage/actions";
import { warning } from "../../../toast/toast"; import { warning } from "../../../toast/toast";

View File

@ -7,7 +7,7 @@ jest.mock("../../../config_storage/actions", () => ({
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import { import {
DevWidget, DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow DevWidget, DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow,
} from "../dev_widget"; } from "../dev_widget";
import { DevSettings } from "../dev_support"; import { DevSettings } from "../dev_support";
import { setWebAppConfigValue } from "../../../config_storage/actions"; import { setWebAppConfigValue } from "../../../config_storage/actions";

View File

@ -1,6 +1,6 @@
import { store } from "../../redux/store"; import { store } from "../../redux/store";
import { import {
getWebAppConfigValue, setWebAppConfigValue getWebAppConfigValue, setWebAppConfigValue,
} from "../../config_storage/actions"; } from "../../config_storage/actions";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";

View File

@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { import {
Widget, WidgetHeader, WidgetBody, Row, Col, BlurableInput Widget, WidgetHeader, WidgetBody, Row, Col, BlurableInput,
} from "../../ui"; } from "../../ui";
import { ToggleButton } from "../../controls/toggle_button"; import { ToggleButton } from "../../controls/toggle_button";
import { setWebAppConfigValue } from "../../config_storage/actions"; import { setWebAppConfigValue } from "../../config_storage/actions";

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget,
} from "./components"; } from "./components";
import { Props } from "./interfaces"; import { Props } from "./interfaces";
import { Page, Row, Col } from "../ui"; import { Page, Row, Col } from "../ui";
@ -47,12 +47,13 @@ export class RawAccount extends React.Component<Props, State> {
(key: keyof User) => (key === "email") && this.setState({ warnThem: true }); (key: keyof User) => (key === "email") && this.setState({ warnThem: true });
onChange = (e: React.FormEvent<HTMLInputElement>) => { onChange = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget; const { value } = e.currentTarget;
if (isKey(name)) { const field = e.currentTarget.name;
this.tempHack(name); if (isKey(field)) {
this.props.dispatch(edit(this.props.user, { [name]: value })); this.tempHack(field);
this.props.dispatch(edit(this.props.user, { [field]: value }));
} else { } else {
throw new Error("Bad key: " + name); throw new Error("Bad key: " + field);
} }
}; };

View File

@ -5,7 +5,7 @@ const mockFeatures = [
storageKey: "weedDetector", storageKey: "weedDetector",
callback: jest.fn(), callback: jest.fn(),
value: false value: false
} },
]; ];
const mocks = { const mocks = {

View File

@ -1,7 +1,7 @@
import { BooleanSetting } from "../../session_keys"; import { BooleanSetting } from "../../session_keys";
import { Content } from "../../constants"; import { Content } from "../../constants";
import { import {
GetWebAppConfigValue, setWebAppConfigValue GetWebAppConfigValue, setWebAppConfigValue,
} from "../../config_storage/actions"; } from "../../config_storage/actions";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
@ -78,7 +78,7 @@ export const fetchLabFeatures =
storageKey: BooleanSetting.user_interface_read_only_mode, storageKey: BooleanSetting.user_interface_read_only_mode,
value: false, value: false,
displayInvert: false, displayInvert: false,
} },
].map(fetchSettingValue(getConfigValue))); ].map(fetchSettingValue(getConfigValue)));
/** Always allow toggling from true => false (deactivate). /** Always allow toggling from true => false (deactivate).

View File

@ -11,7 +11,7 @@ interface LabsFeaturesListProps {
} }
export function LabsFeaturesList(props: LabsFeaturesListProps) { export function LabsFeaturesList(props: LabsFeaturesListProps) {
return <div> return <div className="labs-features-list">
{fetchLabFeatures(props.getConfigValue).map((feature, i) => { {fetchLabFeatures(props.getConfigValue).map((feature, i) => {
const displayValue = feature.displayInvert ? !feature.value : feature.value; const displayValue = feature.displayInvert ? !feature.value : feature.value;
return <Row key={i}> return <Row key={i}>
@ -23,6 +23,7 @@ export function LabsFeaturesList(props: LabsFeaturesListProps) {
</Col> </Col>
<Col xs={2}> <Col xs={2}>
<ToggleButton <ToggleButton
title={t("toggle feature")}
toggleValue={displayValue ? 1 : 0} toggleValue={displayValue ? 1 : 0}
toggleAction={() => props.onToggle(feature) toggleAction={() => props.onToggle(feature)
.then(() => feature.callback && feature.callback())} .then(() => feature.callback && feature.callback())}

View File

@ -9,9 +9,8 @@ interface DataDumpExport { device?: DeviceAccountSettings; }
type Response = AxiosResponse<DataDumpExport | undefined>; type Response = AxiosResponse<DataDumpExport | undefined>;
export function generateFilename({ device }: DataDumpExport): string { export function generateFilename({ device }: DataDumpExport): string {
let name: string; const nameAndId = device ? (device.name + "_" + device.id) : "farmbot";
name = device ? (device.name + "_" + device.id) : "farmbot"; return `export_${nameAndId}.json`.toLowerCase();
return `export_${name}.json`.toLowerCase();
} }
// Thanks, @KOL - https://stackoverflow.com/a/19328891/1064917 // Thanks, @KOL - https://stackoverflow.com/a/19328891/1064917

View File

@ -158,6 +158,10 @@ export class API {
get farmwareInstallationPath() { get farmwareInstallationPath() {
return `${this.baseUrl}/api/farmware_installations/`; return `${this.baseUrl}/api/farmware_installations/`;
} }
/** /api/first_party_farmwares */
get firstPartyFarmwarePath() {
return `${this.baseUrl}/api/first_party_farmwares`;
}
/** /api/alerts/:id */ /** /api/alerts/:id */
get alertPath() { return `${this.baseUrl}/api/alerts/`; } get alertPath() { return `${this.baseUrl}/api/alerts/`; }
/** /api/global_bulletins/:id */ /** /api/global_bulletins/:id */

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Session } from "./session"; import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
const OUTER_STYLE: React.CSSProperties = { const OUTER_STYLE: React.CSSProperties = {
borderRadius: "10px", borderRadius: "10px",
@ -47,7 +48,7 @@ export function Apology(_: {}) {
<li> <li>
<span> <span>
Send a report to our developer team via the&nbsp; Send a report to our developer team via the&nbsp;
<a href="http://forum.farmbot.org/c/software">FarmBot software <a href={ExternalUrl.softwareForum}>FarmBot software
forum</a>. Including additional information (such as steps leading up forum</a>. Including additional information (such as steps leading up
to the error) helps us identify solutions more quickly. to the error) helps us identify solutions more quickly.
</span> </span>

View File

@ -18,7 +18,7 @@ import { validBotLocationData, validFwConfig, validFbosConfig } from "./util";
import { BooleanSetting } from "./session_keys"; import { BooleanSetting } from "./session_keys";
import { getPathArray } from "./history"; import { getPathArray } from "./history";
import { import {
getWebAppConfigValue, GetWebAppConfigValue getWebAppConfigValue, GetWebAppConfigValue,
} from "./config_storage/actions"; } from "./config_storage/actions";
import { takeSortedLogs } from "./logs/state_to_props"; import { takeSortedLogs } from "./logs/state_to_props";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
@ -99,7 +99,7 @@ const MUST_LOAD: ResourceName[] = [
"FarmEvent", "FarmEvent",
"Point", "Point",
"Device", "Device",
"Tool" // Sequence editor needs this for rendering. "Tool", // Sequence editor needs this for rendering.
]; ];
export class RawApp extends React.Component<AppProps, {}> { export class RawApp extends React.Component<AppProps, {}> {

View File

@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
import { import {
fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL, fetchReleases, fetchMinOsFeatureData,
fetchLatestGHBetaRelease fetchLatestGHBetaRelease,
} from "../devices/actions"; } from "../devices/actions";
import { AuthState } from "./interfaces"; import { AuthState } from "./interfaces";
import { ReduxAction } from "../redux/interfaces"; import { ReduxAction } from "../redux/interfaces";
@ -10,12 +10,13 @@ import { API } from "../api";
import { import {
responseFulfilled, responseFulfilled,
responseRejected, responseRejected,
requestFulfilled requestFulfilled,
} from "../interceptors"; } from "../interceptors";
import { Actions } from "../constants"; import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device"; import { connectDevice } from "../connectivity/connect_device";
import { getFirstPartyFarmwareList } from "../farmware/actions"; import { getFirstPartyFarmwareList } from "../farmware/actions";
import { readOnlyInterceptor } from "../read_only_mode"; import { readOnlyInterceptor } from "../read_only_mode";
import { ExternalUrl } from "../external_urls";
export function didLogin(authState: AuthState, dispatch: Function) { export function didLogin(authState: AuthState, dispatch: Function) {
API.setBaseUrl(authState.token.unencoded.iss); API.setBaseUrl(authState.token.unencoded.iss);
@ -24,7 +25,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
beta_os_update_server && beta_os_update_server != "NOT_SET" && beta_os_update_server && beta_os_update_server != "NOT_SET" &&
dispatch(fetchLatestGHBetaRelease(beta_os_update_server)); dispatch(fetchLatestGHBetaRelease(beta_os_update_server));
dispatch(getFirstPartyFarmwareList()); dispatch(getFirstPartyFarmwareList());
dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL)); dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions));
dispatch(setToken(authState)); dispatch(setToken(authState));
Sync.fetchSyncData(dispatch); Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState)); dispatch(connectDevice(authState));

View File

@ -1,58 +1,72 @@
const mockState = {
auth: {
token: {
unencoded: { iss: "http://geocities.com" }
}
}
};
jest.mock("axios", () => ({
interceptors: {
response: { use: jest.fn() },
request: { use: jest.fn() }
},
get() { return Promise.resolve({ data: mockState }); }
}));
jest.mock("../../session", () => ({ jest.mock("../../session", () => ({
Session: { Session: {
fetchStoredToken: jest.fn(), fetchStoredToken: jest.fn(),
getAll: () => undefined, getAll: () => undefined,
clear: jest.fn() clear: jest.fn(),
} }
})); }));
jest.mock("../../auth/actions", () => ({ jest.mock("../../auth/actions", () => ({
didLogin: jest.fn(), didLogin: jest.fn(),
setToken: jest.fn() setToken: jest.fn(),
})); }));
jest.mock("../../refresh_token", () => ({ maybeRefreshToken: jest.fn() }));
let mockTimeout = Promise.resolve({ token: "fake token data" });
jest.mock("promise-timeout", () => ({ timeout: () => mockTimeout }));
import { ready, storeToken } from "../actions"; import { ready, storeToken } from "../actions";
import { setToken, didLogin } from "../../auth/actions"; import { setToken, didLogin } from "../../auth/actions";
import { Session } from "../../session"; import { Session } from "../../session";
import { auth } from "../../__test_support__/fake_state/token"; import { auth } from "../../__test_support__/fake_state/token";
import { fakeState } from "../../__test_support__/fake_state"; import { fakeState } from "../../__test_support__/fake_state";
describe("Actions", () => { describe("ready()", () => {
it("calls didLogin()", () => { it("uses new token", async () => {
jest.resetAllMocks(); const fakeAuth = { token: "fake token data" };
mockTimeout = Promise.resolve(fakeAuth);
const dispatch = jest.fn(); const dispatch = jest.fn();
const thunk = ready(); const thunk = ready();
thunk(dispatch, fakeState); const state = fakeState();
expect(setToken).toHaveBeenCalled(); console.warn = jest.fn();
await thunk(dispatch, () => state);
expect(setToken).toHaveBeenCalledWith(fakeAuth);
expect(didLogin).toHaveBeenCalledWith(fakeAuth, dispatch);
expect(console.warn).not.toHaveBeenCalled();
expect(Session.clear).not.toHaveBeenCalled();
}); });
it("Calls Session.clear() when missing auth", () => { it("uses old token", async () => {
jest.resetAllMocks(); mockTimeout = Promise.reject({ token: "not used" });
const dispatch = jest.fn();
const thunk = ready();
const state = fakeState();
console.warn = jest.fn();
await thunk(dispatch, () => state);
expect(setToken).toHaveBeenLastCalledWith(state.auth);
expect(didLogin).toHaveBeenCalledWith(state.auth, dispatch);
expect(console.warn)
.toHaveBeenCalledWith(expect.stringContaining("Can't refresh token."));
expect(Session.clear).not.toHaveBeenCalled();
});
it("calls Session.clear() when missing auth", () => {
const dispatch = jest.fn(); const dispatch = jest.fn();
const state = fakeState(); const state = fakeState();
delete state.auth; delete state.auth;
const getState = () => state; const getState = () => state;
const thunk = ready(); const thunk = ready();
console.warn = jest.fn();
thunk(dispatch, getState); thunk(dispatch, getState);
expect(setToken).not.toHaveBeenCalled();
expect(didLogin).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
expect(Session.clear).toHaveBeenCalled(); expect(Session.clear).toHaveBeenCalled();
}); });
});
describe("storeToken()", () => {
it("stores token", () => { it("stores token", () => {
const old = auth; const old = auth;
old.token.unencoded.jti = "old"; old.token.unencoded.jti = "old";

View File

@ -1,5 +1,5 @@
import { import {
toggleWebAppBool, getWebAppConfigValue, setWebAppConfigValue toggleWebAppBool, getWebAppConfigValue, setWebAppConfigValue,
} from "../actions"; } from "../actions";
import { BooleanSetting, NumericSetting } from "../../session_keys"; import { BooleanSetting, NumericSetting } from "../../session_keys";
import { edit, save } from "../../api/crud"; import { edit, save } from "../../api/crud";

View File

@ -4,7 +4,7 @@ import {
BooleanConfigKey, BooleanConfigKey,
WebAppConfig, WebAppConfig,
NumberConfigKey, NumberConfigKey,
StringConfigKey StringConfigKey,
} from "farmbot/dist/resources/configs/web_app"; } from "farmbot/dist/resources/configs/web_app";
import { getWebAppConfig } from "../resources/getters"; import { getWebAppConfig } from "../resources/getters";

View File

@ -10,7 +10,7 @@ import { fakeState } from "../../__test_support__/fake_state";
import { GetState } from "../../redux/interfaces"; import { GetState } from "../../redux/interfaces";
import { handleInbound } from "../auto_sync_handle_inbound"; import { handleInbound } from "../auto_sync_handle_inbound";
import { import {
handleCreateOrUpdate handleCreateOrUpdate,
} from "../auto_sync"; } from "../auto_sync";
import { destroyOK } from "../../resources/actions"; import { destroyOK } from "../../resources/actions";
import { SkipMqttData, BadMqttData, UpdateMqttData, DeleteMqttData } from "../interfaces"; import { SkipMqttData, BadMqttData, UpdateMqttData, DeleteMqttData } from "../interfaces";

View File

@ -4,7 +4,7 @@ import {
asTaggedResource, asTaggedResource,
handleCreate, handleCreate,
handleUpdate, handleUpdate,
handleCreateOrUpdate handleCreateOrUpdate,
} from "../auto_sync"; } from "../auto_sync";
import { SpecialStatus, TaggedSequence } from "farmbot"; import { SpecialStatus, TaggedSequence } from "farmbot";
import { Actions } from "../../constants"; import { Actions } from "../../constants";

View File

@ -35,7 +35,7 @@ describe("attachEventListeners", () => {
].map(e => expect(dev.on).toHaveBeenCalledWith(e, expect.any(Function))); ].map(e => expect(dev.on).toHaveBeenCalledWith(e, expect.any(Function)));
[ [
"message", "message",
"reconnect" "reconnect",
].map(e => { ].map(e => {
if (dev.client) { if (dev.client) {
expect(dev.client.on).toHaveBeenCalledWith(e, expect.any(Function)); expect(dev.client.on).toHaveBeenCalledWith(e, expect.any(Function));

View File

@ -1,21 +1,15 @@
jest.mock("../../slow_down", () => { jest.mock("../../slow_down", () => ({
return { slowDown: jest.fn((fn: Function) => fn)
slowDown: jest.fn((fn: Function) => fn),
};
});
jest.mock("../../../devices/actions", () => ({
badVersion: jest.fn(),
EXPECTED_MAJOR: 1,
EXPECTED_MINOR: 0,
})); }));
jest.mock("../../../devices/actions", () => ({ badVersion: jest.fn() }));
import { import {
onStatus, onStatus,
incomingStatus, incomingStatus,
incomingLegacyStatus, incomingLegacyStatus,
onLegacyStatus, onLegacyStatus,
HACKY_FLAGS HACKY_FLAGS,
} from "../../connect_device"; } from "../../connect_device";
import { slowDown } from "../../slow_down"; import { slowDown } from "../../slow_down";
import { fakeState } from "../../../__test_support__/fake_state"; import { fakeState } from "../../../__test_support__/fake_state";
@ -49,8 +43,10 @@ describe("onStatus()", () => {
}); });
it("version ok", () => { it("version ok", () => {
globalConfig.MINIMUM_FBOS_VERSION = "1.0.0";
callOnStatus("1.0.0"); callOnStatus("1.0.0");
expect(badVersion).not.toHaveBeenCalled(); expect(badVersion).not.toHaveBeenCalled();
delete globalConfig.MINIMUM_FBOS_VERSION;
}); });
}); });

View File

@ -20,7 +20,7 @@ import { getDevice } from "../../device";
import { store } from "../../redux/store"; import { store } from "../../redux/store";
import { Actions } from "../../constants"; import { Actions } from "../../constants";
import { import {
startTracking, outstandingRequests, stopTracking, cleanUUID startTracking, outstandingRequests, stopTracking, cleanUUID,
} from "../data_consistency"; } from "../data_consistency";
const unprocessedUuid = "~UU.ID~"; const unprocessedUuid = "~UU.ID~";

View File

@ -8,7 +8,7 @@ jest.mock("../index", () => ({
import { import {
readPing, readPing,
startPinging, startPinging,
PING_INTERVAL PING_INTERVAL,
} from "../ping_mqtt"; } from "../ping_mqtt";
import { Farmbot, RpcRequest, RpcRequestBodyItem } from "farmbot"; import { Farmbot, RpcRequest, RpcRequestBodyItem } from "farmbot";
import { FarmBotInternalConfig } from "farmbot/dist/config"; import { FarmBotInternalConfig } from "farmbot/dist/config";

View File

@ -41,7 +41,7 @@ describe("connectivity reducer", () => {
it("broadcasts PING_OK", () => { it("broadcasts PING_OK", () => {
pingOK("yep", 123); pingOK("yep", 123);
expect(store.dispatch).toHaveBeenCalledWith({ expect(store.dispatch).toHaveBeenCalledWith({
payload: { at: 123, id: "yep", }, payload: { at: 123, id: "yep" },
type: "PING_OK", type: "PING_OK",
}); });
}); });

View File

@ -4,7 +4,7 @@ import { TaggedResource, SpecialStatus } from "farmbot";
import { overwrite, init } from "../api/crud"; import { overwrite, init } from "../api/crud";
import { handleInbound } from "./auto_sync_handle_inbound"; import { handleInbound } from "./auto_sync_handle_inbound";
import { import {
SyncPayload, MqttDataResult, Reason, UpdateMqttData SyncPayload, MqttDataResult, Reason, UpdateMqttData,
} from "./interfaces"; } from "./interfaces";
import { outstandingRequests } from "./data_consistency"; import { outstandingRequests } from "./data_consistency";
import { newTaggedResource } from "../sync/actions"; import { newTaggedResource } from "../sync/actions";

View File

@ -8,13 +8,7 @@ import { success, error, info, warning, fun, busy } from "../toast/toast";
import { HardwareState } from "../devices/interfaces"; import { HardwareState } from "../devices/interfaces";
import { GetState, ReduxAction } from "../redux/interfaces"; import { GetState, ReduxAction } from "../redux/interfaces";
import { Content, Actions } from "../constants"; import { Content, Actions } from "../constants";
import { import { commandOK, badVersion, commandErr } from "../devices/actions";
EXPECTED_MAJOR,
EXPECTED_MINOR,
commandOK,
badVersion,
commandErr
} from "../devices/actions";
import { init } from "../api/crud"; import { init } from "../api/crud";
import { AuthState } from "../auth/interfaces"; import { AuthState } from "../auth/interfaces";
import { autoSync } from "./auto_sync"; import { autoSync } from "./auto_sync";
@ -123,7 +117,7 @@ const setBothUp = () => bothUp();
const legacyChecks = (getState: GetState) => { const legacyChecks = (getState: GetState) => {
const { controller_version } = getState().bot.hardware.informational_settings; const { controller_version } = getState().bot.hardware.informational_settings;
if (HACKY_FLAGS.needVersionCheck && controller_version) { if (HACKY_FLAGS.needVersionCheck && controller_version) {
const IS_OK = versionOK(controller_version, EXPECTED_MAJOR, EXPECTED_MINOR); const IS_OK = versionOK(controller_version);
if (!IS_OK) { badVersion(); } if (!IS_OK) { badVersion(); }
HACKY_FLAGS.needVersionCheck = false; HACKY_FLAGS.needVersionCheck = false;
} }

View File

@ -3,7 +3,7 @@ import {
actOnChannelName, actOnChannelName,
showLogOnScreen, showLogOnScreen,
speakLogAloud, speakLogAloud,
initLog initLog,
} from "./connect_device"; } from "./connect_device";
import { GetState } from "../redux/interfaces"; import { GetState } from "../redux/interfaces";
import { Log } from "farmbot/dist/resources/api_resources"; import { Log } from "farmbot/dist/resources/api_resources";

View File

@ -4,7 +4,7 @@ import {
dispatchNetworkUp, dispatchNetworkUp,
dispatchQosStart, dispatchQosStart,
pingOK, pingOK,
pingNO pingNO,
} from "./index"; } from "./index";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
import axios from "axios"; import axios from "axios";

View File

@ -2,7 +2,7 @@ import { generateReducer } from "../redux/generate_reducer";
import { Actions } from "../constants"; import { Actions } from "../constants";
import { import {
ConnectionState, ConnectionState,
EdgeStatus EdgeStatus,
} from "./interfaces"; } from "./interfaces";
import { startPing, completePing, failPing } from "../devices/connectivity/qos"; import { startPing, completePing, failPing } from "../devices/connectivity/qos";

View File

@ -39,8 +39,8 @@ export namespace ToolTips {
few sequences to verify that everything works as expected.`); few sequences to verify that everything works as expected.`);
export const PIN_BINDINGS = export const PIN_BINDINGS =
trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is trim(`Assign an action or sequence to execute when a Raspberry Pi
activated.`); GPIO pin is activated.`);
export const PIN_BINDING_WARNING = export const PIN_BINDING_WARNING =
trim(`Warning: Binding to a pin without a physical button and trim(`Warning: Binding to a pin without a physical button and
@ -51,24 +51,38 @@ export namespace ToolTips {
trim(`Diagnose connectivity issues with FarmBot and the browser.`); trim(`Diagnose connectivity issues with FarmBot and the browser.`);
// Hardware Settings: Homing and Calibration // Hardware Settings: Homing and Calibration
export const HOMING = export const HOMING_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis (find zero).`); trim(`If encoders or end-stops are enabled, home axis (find zero).`);
export const CALIBRATION = export const HOMING_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, home axis
(find zero).`);
export const CALIBRATION_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis and determine trim(`If encoders or end-stops are enabled, home axis and determine
maximum.`); maximum.`);
export const CALIBRATION_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, home axis and
determine maximum.`);
export const SET_ZERO_POSITION = export const SET_ZERO_POSITION =
trim(`Set the current location as zero.`); trim(`Set the current location as zero.`);
export const FIND_HOME_ON_BOOT = export const FIND_HOME_ON_BOOT_ENCODERS =
trim(`If encoders or end-stops are enabled, find the home position trim(`If encoders or end-stops are enabled, find the home position
when the device powers on. when the device powers on. Warning! This will perform homing on all
Warning! This will perform homing on all axes when the axes when the device powers on. Encoders or endstops must be enabled.
device powers on. Encoders or endstops must be enabled.
It is recommended to make sure homing works properly before enabling It is recommended to make sure homing works properly before enabling
this feature. (default: disabled)`); this feature. (default: disabled)`);
export const FIND_HOME_ON_BOOT_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, find the home
position when the device powers on. Warning! This will perform homing
on all axes when the device powers on. Stall detection or endstops
must be enabled. It is recommended to make sure homing works properly
before enabling this feature. (default: disabled)`);
export const STOP_AT_HOME = export const STOP_AT_HOME =
trim(`Stop at the home location of the axis. (default: disabled)`); trim(`Stop at the home location of the axis. (default: disabled)`);
@ -85,18 +99,7 @@ export namespace ToolTips {
trim(`Set the length of each axis to provide software limits. trim(`Set the length of each axis to provide software limits.
Used only if STOP AT MAX is enabled. (default: 0 (disabled))`); Used only if STOP AT MAX is enabled. (default: 0 (disabled))`);
export const TIMEOUT_AFTER =
trim(`Amount of time to wait for a command to execute before stopping.
(default: 120s)`);
// Hardware Settings: Motors // Hardware Settings: Motors
export const MAX_MOVEMENT_RETRIES =
trim(`Number of times to retry a movement before stopping. (default: 3)`);
export const E_STOP_ON_MOV_ERR =
trim(`Emergency stop if movement is not complete after the maximum
number of retries. (default: disabled)`);
export const MAX_SPEED = export const MAX_SPEED =
trim(`Maximum travel speed after acceleration in millimeters per second. trim(`Maximum travel speed after acceleration in millimeters per second.
(default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`); (default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`);
@ -132,18 +135,22 @@ export namespace ToolTips {
export const MOTOR_CURRENT = export const MOTOR_CURRENT =
trim(`Motor current in milliamps. (default: 600)`); trim(`Motor current in milliamps. (default: 600)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENABLE_X2_MOTOR = export const ENABLE_X2_MOTOR =
trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS. trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS.
(default: enabled)`); (default: enabled)`);
// Hardware Settings: Encoders and Endstops // Hardware Settings: Encoders / Stall Detection
export const ENABLE_ENCODERS = export const ENABLE_ENCODERS =
trim(`Enable use of rotary encoders for stall detection, trim(`Enable use of rotary encoders for stall detection,
calibration and homing. (default: enabled)`); calibration and homing. (default: enabled)`);
export const ENABLE_STALL_DETECTION =
trim(`Enable use of motor stall detection for detecting missed steps,
calibration and homing. (default: enabled)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENCODER_POSITIONING = export const ENCODER_POSITIONING =
trim(`Use encoders for positioning. (default: disabled)`); trim(`Use encoders for positioning. (default: disabled)`);
@ -151,17 +158,22 @@ export namespace ToolTips {
trim(`Reverse the direction of encoder position reading. trim(`Reverse the direction of encoder position reading.
(default: disabled)`); (default: disabled)`);
export const MAX_MISSED_STEPS = export const MAX_MISSED_STEPS_ENCODERS =
trim(`Number of steps missed (determined by encoder) before motor is trim(`Number of steps missed (determined by encoder) before motor is
considered to have stalled. (default: 5)`); considered to have stalled. (default: 5)`);
export const ENCODER_MISSED_STEP_DECAY = export const MAX_MISSED_STEPS_STALL_DETECTION =
trim(`Number of steps missed (determined by motor stall detection) before
motor is considered to have stalled. (default: 5)`);
export const MISSED_STEP_DECAY =
trim(`Reduction to missed step total for every good step. (default: 5)`); trim(`Reduction to missed step total for every good step. (default: 5)`);
export const ENCODER_SCALING = export const ENCODER_SCALING =
trim(`encoder scaling factor = 10000 * (motor resolution * microsteps) trim(`encoder scaling factor = 10000 * (motor resolution * microsteps)
/ (encoder resolution). (default: 5556 (10000*200/360))`); / (encoder resolution). (default: 5556 (10000*200/360))`);
// Hardware Settings: Endstops
export const ENABLE_ENDSTOPS = export const ENABLE_ENDSTOPS =
trim(`Enable use of electronic end-stops for end detection, trim(`Enable use of electronic end-stops for end detection,
calibration and homing. (default: disabled)`); calibration and homing. (default: disabled)`);
@ -173,6 +185,18 @@ export namespace ToolTips {
trim(`Invert axis end-stops. Enable for normally closed (NC), trim(`Invert axis end-stops. Enable for normally closed (NC),
disable for normally open (NO). (default: disabled)`); disable for normally open (NO). (default: disabled)`);
// Hardware Settings: Error Handling
export const TIMEOUT_AFTER =
trim(`Amount of time to wait for a command to execute before stopping.
(default: 120s)`);
export const MAX_MOVEMENT_RETRIES =
trim(`Number of times to retry a movement before stopping. (default: 3)`);
export const E_STOP_ON_MOV_ERR =
trim(`Emergency stop if movement is not complete after the maximum
number of retries. (default: disabled)`);
// Hardware Settings: Pin Guard // Hardware Settings: Pin Guard
export const PIN_GUARD_PIN_NUMBER = export const PIN_GUARD_PIN_NUMBER =
trim(`The number of the pin to guard. This pin will be set to the specified trim(`The number of the pin to guard. This pin will be set to the specified
@ -263,8 +287,12 @@ export namespace ToolTips {
export const FIND_HOME = export const FIND_HOME =
trim(`The Find Home step instructs the device to perform a homing trim(`The Find Home step instructs the device to perform a homing
command (using encoders or endstops) to find and set zero for command (using encoders, stall detection, or endstops) to find and set
the chosen axis or axes.`); zero for the chosen axis or axes.`);
export const CALIBRATE =
trim(`If encoders, stall detection, or end-stops are enabled,
home axis and determine maximum.`);
export const IF = export const IF =
trim(`Execute a sequence if a condition is satisfied. If the condition trim(`Execute a sequence if a condition is satisfied. If the condition
@ -620,8 +648,8 @@ export namespace Content {
trim(`Restart the Farmduino or Arduino firmware.`); trim(`Restart the Farmduino or Arduino firmware.`);
export const OS_AUTO_UPDATE = export const OS_AUTO_UPDATE =
trim(`When enabled, FarmBot OS will periodically check for, download, trim(`When enabled, FarmBot OS will automatically download and install
and install updates automatically.`); software updates at the chosen time.`);
export const AUTO_SYNC = export const AUTO_SYNC =
trim(`When enabled, device resources such as sequences and regimens trim(`When enabled, device resources such as sequences and regimens
@ -635,7 +663,7 @@ export namespace Content {
back on, unplug FarmBot and plug it back in.`); back on, unplug FarmBot and plug it back in.`);
export const OS_BETA_RELEASES = export const OS_BETA_RELEASES =
trim(`Warning! Opting in to FarmBot OS beta releases may reduce trim(`Warning! Leaving the stable FarmBot OS release channel may reduce
FarmBot system stability. Are you sure?`); FarmBot system stability. Are you sure?`);
export const DIAGNOSTIC_CHECK = export const DIAGNOSTIC_CHECK =
@ -674,9 +702,9 @@ export namespace Content {
trim(`FarmBot sent a malformed message. You may need to upgrade trim(`FarmBot sent a malformed message. You may need to upgrade
FarmBot OS. Please upgrade FarmBot OS and log back in.`); FarmBot OS. Please upgrade FarmBot OS and log back in.`);
export const OLD_FBOS_REC_UPGRADE = trim(`Your version of FarmBot OS is export const OLD_FBOS_REC_UPGRADE =
outdated and will soon no longer be supported. Please update your device as trim(`Your version of FarmBot OS is outdated and will soon no longer
soon as possible.`); be supported. Please update your device as soon as possible.`);
export const EXPERIMENTAL_WARNING = export const EXPERIMENTAL_WARNING =
trim(`Warning! This is an EXPERIMENTAL feature. This feature may be trim(`Warning! This is an EXPERIMENTAL feature. This feature may be
@ -715,8 +743,8 @@ export namespace Content {
export const END_DETECTION_DISABLED = export const END_DETECTION_DISABLED =
trim(`This command will not execute correctly because you do not have trim(`This command will not execute correctly because you do not have
encoders or endstops enabled for the chosen axis. Enable endstops or encoders, stall detection, or endstops enabled for the chosen axis.
encoders from the Device page for: `); Enable endstops, encoders, or stall detection from the Device page for: `);
export const IN_USE = export const IN_USE =
trim(`Used in another resource. Protected from deletion.`); trim(`Used in another resource. Protected from deletion.`);
@ -784,7 +812,10 @@ export namespace Content {
trim(`add this crop on OpenFarm?`); trim(`add this crop on OpenFarm?`);
export const NO_TOOLS = export const NO_TOOLS =
trim(`Press "+" to add a new tool.`); trim(`Press "+" to add a new tool or seed container.`);
export const NO_SEED_CONTAINERS =
trim(`Press "+" to add a seed container.`);
export const MOUNTED_TOOL = export const MOUNTED_TOOL =
trim(`The tool currently mounted to the UTM can be set here or by using trim(`The tool currently mounted to the UTM can be set here or by using
@ -859,12 +890,23 @@ export namespace TourContent {
selecting one, and dragging it into the garden.`); selecting one, and dragging it into the garden.`);
export const ADD_TOOLS = export const ADD_TOOLS =
trim(`Press edit and then the + button to add tools and seed containers.`); trim(`Press the + button to add tools and seed containers.`);
export const ADD_SEED_CONTAINERS =
trim(`Press the + button to add seed containers.`);
export const ADD_TOOLS_AND_SLOTS =
trim(`Press the + button to add tools and seed containers. Then create
slots for them to by pressing the slot + button.`);
export const ADD_SEED_CONTAINERS_AND_SLOTS =
trim(`Press the + button to add seed containers. Then create
slots for them to by pressing the slot + button.`);
export const ADD_TOOLS_SLOTS = export const ADD_TOOLS_SLOTS =
trim(`Add the newly created tools and seed containers to the trim(`Add the newly created tools and seed containers to the
corresponding tool slots on FarmBot: corresponding slots on FarmBot:
press edit and then + to create a tool slot.`); press the + button to create a slot.`);
export const ADD_PERIPHERALS = export const ADD_PERIPHERALS =
trim(`Press edit and then the + button to add peripherals.`); trim(`Press edit and then the + button to add peripherals.`);
@ -902,6 +944,103 @@ export namespace TourContent {
trim(`Toggle various settings to customize your web app experience.`); trim(`Toggle various settings to customize your web app experience.`);
} }
export enum DeviceSetting {
// Homing and calibration
homingAndCalibration = `Homing and Calibration`,
homing = `Homing`,
calibration = `Calibration`,
setZeroPosition = `Set Zero Position`,
findHomeOnBoot = `Find Home on Boot`,
stopAtHome = `Stop at Home`,
stopAtMax = `Stop at Max`,
negativeCoordinatesOnly = `Negative Coordinates Only`,
axisLength = `Axis Length (mm)`,
// Motors
motors = `Motors`,
maxSpeed = `Max Speed (mm/s)`,
homingSpeed = `Homing Speed (mm/s)`,
minimumSpeed = `Minimum Speed (mm/s)`,
accelerateFor = `Accelerate for (mm)`,
stepsPerMm = `Steps per MM`,
microstepsPerStep = `Microsteps per step`,
alwaysPowerMotors = `Always Power Motors`,
invertMotors = `Invert Motors`,
motorCurrent = `Motor Current`,
enable2ndXMotor = `Enable 2nd X Motor`,
invert2ndXMotor = `Invert 2nd X Motor`,
// Encoders / Stall Detection
encoders = `Encoders`,
stallDetection = `Stall Detection`,
enableEncoders = `Enable Encoders`,
enableStallDetection = `Enable Stall Detection`,
stallSensitivity = `Stall Sensitivity`,
useEncodersForPositioning = `Use Encoders for Positioning`,
invertEncoders = `Invert Encoders`,
maxMissedSteps = `Max Missed Steps`,
missedStepDecay = `Missed Step Decay`,
encoderScaling = `Encoder Scaling`,
// Endstops
endstops = `Endstops`,
enableEndstops = `Enable Endstops`,
swapEndstops = `Swap Endstops`,
invertEndstops = `Invert Endstops`,
// Error handling
errorHandling = `Error Handling`,
timeoutAfter = `Timeout after (seconds)`,
maxRetries = `Max Retries`,
estopOnMovementError = `E-Stop on Movement Error`,
// Pin Guard
pinGuard = `Pin Guard`,
// Danger Zone
dangerZone = `Danger Zone`,
resetHardwareParams = `Reset hardware parameter defaults`,
// Pin Bindings
pinBindings = `Pin Bindings`,
// FarmBot OS
farmbot = `FarmBot`,
name = `name`,
timezone = `timezone`,
camera = `camera`,
firmware = `Firmware`,
applySoftwareUpdates = `update time`,
farmbotOSAutoUpdate = `auto update`,
farmbotOS = `Farmbot OS`,
autoSync = `Auto Sync`,
bootSequence = `Boot Sequence`,
// Power and Reset
powerAndReset = `Power and Reset`,
restartFarmbot = `Restart Farmbot`,
shutdownFarmbot = `Shutdown Farmbot`,
restartFirmware = `Restart Firmware`,
factoryReset = `Factory Reset`,
autoFactoryReset = `Automatic Factory Reset`,
connectionAttemptPeriod = `Connection Attempt Period`,
changeOwnership = `Change Ownership`,
// Farm Designer
farmDesigner = `Farm Designer`,
animations = `Plant animations`,
trail = `Virtual FarmBot trail`,
dynamicMap = `Dynamic map size`,
mapSize = `Map size`,
rotateMap = `Rotate map`,
mapOrigin = `Map origin`,
confirmPlantDeletion = `Confirm plant deletion`,
// Firmware
firmwareSection = `Firmware`,
flashFirmware = `Flash firmware`,
}
export namespace DiagnosticMessages { export namespace DiagnosticMessages {
export const OK = trim(`All systems nominal.`); export const OK = trim(`All systems nominal.`);
@ -924,8 +1063,7 @@ export namespace DiagnosticMessages {
but we have no recent record of FarmBot connecting to the internet. but we have no recent record of FarmBot connecting to the internet.
This usually happens because of poor WiFi connectivity in the garden, This usually happens because of poor WiFi connectivity in the garden,
a bad password during configuration, a very long power outage, or a bad password during configuration, a very long power outage, or
blocked ports on FarmBot's local network. Please refer IT staff to blocked ports on FarmBot's local network. Please refer IT staff to:`);
https://software.farm.bot/docs/for-it-security-professionals`);
export const NO_WS_AVAILABLE = trim(`You are either offline, using a web export const NO_WS_AVAILABLE = trim(`You are either offline, using a web
browser that does not support WebSockets, or are behind a firewall that browser that does not support WebSockets, or are behind a firewall that

View File

@ -3,7 +3,7 @@ import { mount } from "enzyme";
import { RawControls as Controls } from "../controls"; import { RawControls as Controls } from "../controls";
import { bot } from "../../__test_support__/fake_state/bot"; import { bot } from "../../__test_support__/fake_state/bot";
import { import {
fakePeripheral, fakeWebcamFeed, fakeSensor fakePeripheral, fakeWebcamFeed, fakeSensor,
} from "../../__test_support__/fake_state/resources"; } from "../../__test_support__/fake_state/resources";
import { Dictionary } from "farmbot"; import { Dictionary } from "farmbot";
import { Props } from "../interfaces"; import { Props } from "../interfaces";

View File

@ -3,17 +3,19 @@ import { Row, Col } from "../ui/index";
import { AxisDisplayGroupProps } from "./interfaces"; import { AxisDisplayGroupProps } from "./interfaces";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { Xyz } from "farmbot";
const Axis = ({ val }: { val: number | undefined }) => <Col xs={3}> const Axis = ({ axis, val }: { val: number | undefined, axis: Xyz }) =>
<input disabled value={isNumber(val) ? val : "---"} /> <Col xs={3}>
</Col>; <input disabled name={axis} value={isNumber(val) ? val : "---"} />
</Col>;
export const AxisDisplayGroup = ({ position, label }: AxisDisplayGroupProps) => { export const AxisDisplayGroup = ({ position, label }: AxisDisplayGroupProps) => {
const { x, y, z } = position; const { x, y, z } = position;
return <Row> return <Row>
<Axis val={x} /> <Axis axis={"x"} val={x} />
<Axis val={y} /> <Axis axis={"y"} val={y} />
<Axis val={z} /> <Axis axis={"z"} val={z} />
<Col xs={3}> <Col xs={3}>
<label> <label>
{t(label)} {t(label)}

View File

@ -10,6 +10,7 @@ import { Move } from "./move/move";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { SensorReadings } from "./sensor_readings/sensor_readings"; import { SensorReadings } from "./sensor_readings/sensor_readings";
import { isBotOnline } from "../devices/must_be_online"; import { isBotOnline } from "../devices/must_be_online";
import { hasSensors } from "../devices/components/firmware_hardware_support";
/** Controls page. */ /** Controls page. */
export class RawControls extends React.Component<Props, {}> { export class RawControls extends React.Component<Props, {}> {
@ -24,7 +25,8 @@ export class RawControls extends React.Component<Props, {}> {
} }
get hideSensors() { get hideSensors() {
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors); return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors)
|| !hasSensors(this.props.firmwareHardware);
} }
move = () => <Move move = () => <Move
@ -38,6 +40,7 @@ export class RawControls extends React.Component<Props, {}> {
getWebAppConfigVal={this.props.getWebAppConfigVal} /> getWebAppConfigVal={this.props.getWebAppConfigVal} />
peripherals = () => <Peripherals peripherals = () => <Peripherals
firmwareHardware={this.props.firmwareHardware}
bot={this.props.bot} bot={this.props.bot}
peripherals={this.props.peripherals} peripherals={this.props.peripherals}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
@ -50,6 +53,7 @@ export class RawControls extends React.Component<Props, {}> {
sensors = () => this.hideSensors sensors = () => this.hideSensors
? <div id="hidden-sensors-widget" /> ? <div id="hidden-sensors-widget" />
: <Sensors : <Sensors
firmwareHardware={this.props.firmwareHardware}
bot={this.props.bot} bot={this.props.bot}
sensors={this.props.sensors} sensors={this.props.sensors}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}

View File

@ -1,12 +1,12 @@
import { import {
BotState, Xyz, BotPosition, ShouldDisplay, UserEnv BotState, Xyz, BotPosition, ShouldDisplay, UserEnv,
} from "../devices/interfaces"; } from "../devices/interfaces";
import { Vector3, McuParams, FirmwareHardware } from "farmbot/dist"; import { Vector3, McuParams, FirmwareHardware } from "farmbot/dist";
import { import {
TaggedWebcamFeed, TaggedWebcamFeed,
TaggedPeripheral, TaggedPeripheral,
TaggedSensor, TaggedSensor,
TaggedSensorReading TaggedSensorReading,
} from "farmbot"; } from "farmbot";
import { NetworkState } from "../connectivity/interfaces"; import { NetworkState } from "../connectivity/interfaces";
import { GetWebAppConfigValue } from "../config_storage/actions"; import { GetWebAppConfigValue } from "../config_storage/actions";

View File

@ -15,12 +15,14 @@ export function KeyValEditRow(p: Props) {
return <Row> return <Row>
<Col xs={6}> <Col xs={6}>
<input type="text" <input type="text"
name="label"
placeholder={p.labelPlaceholder} placeholder={p.labelPlaceholder}
value={p.label} value={p.label}
onChange={p.onLabelChange} /> onChange={p.onLabelChange} />
</Col> </Col>
<Col xs={4}> <Col xs={4}>
<input type={p.valueType} <input type={p.valueType}
name="value"
value={p.value} value={p.value}
placeholder={p.valuePlaceholder} placeholder={p.valuePlaceholder}
onChange={p.onValueChange} /> onChange={p.onValueChange} />

View File

@ -1,5 +1,5 @@
import { import {
calcMicrostepsPerMm, calculateAxialLengths calcMicrostepsPerMm, calculateAxialLengths,
} from "../direction_axes_props"; } from "../direction_axes_props";
import { fakeFirmwareConfig } from "../../../__test_support__/fake_state/resources"; import { fakeFirmwareConfig } from "../../../__test_support__/fake_state/resources";

View File

@ -9,7 +9,7 @@ jest.mock("../../../device", () => ({
import * as React from "react"; import * as React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { import {
DirectionButton, directionDisabled, calculateDistance DirectionButton, directionDisabled, calculateDistance,
} from "../direction_button"; } from "../direction_button";
import { DirectionButtonProps } from "../interfaces"; import { DirectionButtonProps } from "../interfaces";

View File

@ -9,7 +9,7 @@ import * as React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { BooleanSetting } from "../../../session_keys"; import { BooleanSetting } from "../../../session_keys";
import { import {
moveWidgetSetting, MoveWidgetSettingsMenu, MoveWidgetSettingsMenuProps moveWidgetSetting, MoveWidgetSettingsMenu, MoveWidgetSettingsMenuProps,
} from "../settings_menu"; } from "../settings_menu";
describe("moveWidgetSetting()", () => { describe("moveWidgetSetting()", () => {

View File

@ -7,7 +7,7 @@ import { AxisInputBoxGroup } from "../axis_input_box_group";
import { GetWebAppBool } from "./interfaces"; import { GetWebAppBool } from "./interfaces";
import { BooleanSetting } from "../../session_keys"; import { BooleanSetting } from "../../session_keys";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support"; import { hasEncoders } from "../../devices/components/firmware_hardware_support";
import { FirmwareHardware } from "farmbot"; import { FirmwareHardware } from "farmbot";
export interface BotPositionRowsProps { export interface BotPositionRowsProps {
@ -19,7 +19,7 @@ export interface BotPositionRowsProps {
export const BotPositionRows = (props: BotPositionRowsProps) => { export const BotPositionRows = (props: BotPositionRowsProps) => {
const { locationData, getValue, arduinoBusy } = props; const { locationData, getValue, arduinoBusy } = props;
return <div> return <div className={"bot-position-rows"}>
<Row> <Row>
<Col xs={3}> <Col xs={3}>
<label>{t("X AXIS")}</label> <label>{t("X AXIS")}</label>
@ -34,12 +34,12 @@ export const BotPositionRows = (props: BotPositionRowsProps) => {
<AxisDisplayGroup <AxisDisplayGroup
position={locationData.position} position={locationData.position}
label={t("Motor Coordinates (mm)")} /> label={t("Motor Coordinates (mm)")} />
{!isExpressBoard(props.firmwareHardware) && {hasEncoders(props.firmwareHardware) &&
getValue(BooleanSetting.scaled_encoders) && getValue(BooleanSetting.scaled_encoders) &&
<AxisDisplayGroup <AxisDisplayGroup
position={locationData.scaled_encoders} position={locationData.scaled_encoders}
label={t("Scaled Encoder (mm)")} />} label={t("Scaled Encoder (mm)")} />}
{!isExpressBoard(props.firmwareHardware) && {hasEncoders(props.firmwareHardware) &&
getValue(BooleanSetting.raw_encoders) && getValue(BooleanSetting.raw_encoders) &&
<AxisDisplayGroup <AxisDisplayGroup
position={locationData.raw_encoders} position={locationData.raw_encoders}

View File

@ -6,7 +6,7 @@ import { getDevice } from "../../device";
import { buildDirectionProps } from "./direction_axes_props"; import { buildDirectionProps } from "./direction_axes_props";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { import {
cameraBtnProps cameraBtnProps,
} from "../../devices/components/fbos_settings/camera_selection"; } from "../../devices/components/fbos_settings/camera_selection";
const DEFAULT_STEP_SIZE = 100; const DEFAULT_STEP_SIZE = 100;

View File

@ -22,7 +22,7 @@ export const JogControlsGroup = (props: JogControlsGroupProps) => {
const { const {
dispatch, stepSize, botPosition, getValue, arduinoBusy, firmwareSettings dispatch, stepSize, botPosition, getValue, arduinoBusy, firmwareSettings
} = props; } = props;
return <div> return <div className={"jog-controls-group"}>
<label className="text-center"> <label className="text-center">
{t("MOVE AMOUNT (mm)")} {t("MOVE AMOUNT (mm)")}
</label> </label>

View File

@ -4,7 +4,7 @@ import moment from "moment";
import { BotLocationData, BotPosition } from "../../devices/interfaces"; import { BotLocationData, BotPosition } from "../../devices/interfaces";
import { trim } from "../../util"; import { trim } from "../../util";
import { import {
cloneDeep, max, get, isNumber, isEqual, takeRight, ceil, range cloneDeep, max, get, isNumber, isEqual, takeRight, ceil, range,
} from "lodash"; } from "lodash";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
@ -46,9 +46,9 @@ const getLastEntry = (): Entry | undefined => {
const findYLimit = (): number => { const findYLimit = (): number => {
const array = getArray(); const array = getArray();
const arrayAbsMax = max(array.map(entry => const arrayAbsMax = max(array.map(entry =>
max(["position", "scaled_encoders"].map((name: LocationName) => max(["position", "scaled_encoders"].map((key: LocationName) =>
max(["x", "y", "z"].map((axis: Xyz) => max(["x", "y", "z"].map((axis: Xyz) =>
Math.abs(entry.locationData[name][axis] || 0) + 1)))))); Math.abs(entry.locationData[key][axis] || 0) + 1))))));
return Math.max(ceil(arrayAbsMax || 0, -2), DEFAULT_Y_MAX); return Math.max(ceil(arrayAbsMax || 0, -2), DEFAULT_Y_MAX);
}; };
@ -80,19 +80,19 @@ const getPaths = (): Paths => {
const paths = newPaths(); const paths = newPaths();
if (last) { if (last) {
getReversedArray().map(entry => { getReversedArray().map(entry => {
["position", "scaled_encoders"].map((name: LocationName) => { ["position", "scaled_encoders"].map((key: LocationName) => {
["x", "y", "z"].map((axis: Xyz) => { ["x", "y", "z"].map((axis: Xyz) => {
const lastPos = last.locationData[name][axis]; const lastPos = last.locationData[key][axis];
const pos = entry.locationData[name][axis]; const pos = entry.locationData[key][axis];
if (isNumber(lastPos) && isFinite(lastPos) if (isNumber(lastPos) && isFinite(lastPos)
&& isNumber(maxY) && isNumber(pos)) { && isNumber(maxY) && isNumber(pos)) {
if (!paths[name][axis].startsWith("M")) { if (!paths[key][axis].startsWith("M")) {
const yStart = -lastPos / maxY * HEIGHT / 2; const yStart = -lastPos / maxY * HEIGHT / 2;
paths[name][axis] = `M ${MAX_X},${yStart} `; paths[key][axis] = `M ${MAX_X},${yStart} `;
} }
const x = MAX_X - (last.timestamp - entry.timestamp); const x = MAX_X - (last.timestamp - entry.timestamp);
const y = -pos / maxY * HEIGHT / 2; const y = -pos / maxY * HEIGHT / 2;
paths[name][axis] += `L ${x},${y} `; paths[key][axis] += `L ${x},${y} `;
} }
}); });
}); });
@ -154,12 +154,12 @@ const PlotLines = ({ locationData }: { locationData: BotLocationData }) => {
updateArray({ timestamp: moment().unix(), locationData }); updateArray({ timestamp: moment().unix(), locationData });
const paths = getPaths(); const paths = getPaths();
return <g id="plot_lines"> return <g id="plot_lines">
{["position", "scaled_encoders"].map((name: LocationName) => {["position", "scaled_encoders"].map((key: LocationName) =>
["x", "y", "z"].map((axis: Xyz) => ["x", "y", "z"].map((axis: Xyz) =>
<path key={name + axis} fill={"none"} <path key={key + axis} fill={"none"}
stroke={COLOR_LOOKUP[axis]} strokeWidth={LINEWIDTH_LOOKUP[name]} stroke={COLOR_LOOKUP[axis]} strokeWidth={LINEWIDTH_LOOKUP[key]}
strokeLinecap={"round"} strokeLinejoin={"round"} strokeLinecap={"round"} strokeLinejoin={"round"}
d={paths[name][axis]} />))} d={paths[key][axis]} />))}
</g>; </g>;
}; };

View File

@ -6,7 +6,7 @@ import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { DevSettings } from "../../account/dev/dev_support"; import { DevSettings } from "../../account/dev/dev_support";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { FirmwareHardware } from "farmbot"; import { FirmwareHardware } from "farmbot";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support"; import { hasEncoders } from "../../devices/components/firmware_hardware_support";
export const moveWidgetSetting = export const moveWidgetSetting =
(toggle: ToggleWebAppBool, getValue: GetWebAppBool) => (toggle: ToggleWebAppBool, getValue: GetWebAppBool) =>
@ -27,7 +27,7 @@ export interface MoveWidgetSettingsMenuProps {
} }
export const MoveWidgetSettingsMenu = ( export const MoveWidgetSettingsMenu = (
{ toggle, getValue, firmwareHardware }: MoveWidgetSettingsMenuProps { toggle, getValue, firmwareHardware }: MoveWidgetSettingsMenuProps,
) => { ) => {
const Setting = moveWidgetSetting(toggle, getValue); const Setting = moveWidgetSetting(toggle, getValue);
return <div className="move-settings-menu"> return <div className="move-settings-menu">
@ -36,7 +36,7 @@ export const MoveWidgetSettingsMenu = (
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} /> <Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} /> <Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
{!isExpressBoard(firmwareHardware) && {hasEncoders(firmwareHardware) &&
<div className="display-encoder-data"> <div className="display-encoder-data">
<p>{t("Display Encoder Data")}</p> <p>{t("Display Encoder Data")}</p>
<Setting <Setting
@ -56,7 +56,7 @@ export const MoveWidgetSettingsMenu = (
setting={BooleanSetting.home_button_homing} /> setting={BooleanSetting.home_button_homing} />
{DevSettings.futureFeaturesEnabled() && {DevSettings.futureFeaturesEnabled() &&
<div> <div className={"motor-position-plot-setting-row"}>
<p>{t("Motor position plot")}</p> <p>{t("Motor position plot")}</p>
<Setting <Setting
label={t("show")} label={t("show")}

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import { StepSizeSelectorProps } from "./interfaces"; import { StepSizeSelectorProps } from "./interfaces";
import { first, last } from "lodash"; import { first, last } from "lodash";
import { t } from "../../i18next_wrapper";
export class StepSizeSelector extends React.Component<StepSizeSelectorProps, {}> { export class StepSizeSelector extends React.Component<StepSizeSelectorProps, {}> {
cssForIndex(num: number) { cssForIndex(num: number) {
@ -20,16 +21,13 @@ export class StepSizeSelector extends React.Component<StepSizeSelectorProps, {}>
render() { render() {
return <div className="move-amount-wrapper"> return <div className="move-amount-wrapper">
{ {this.props.choices.map((item: number, inx: number) =>
this.props.choices.map( <button key={inx}
(item: number, inx: number) => <button title={t("{{ amount }}mm", { amount: item })}
className={this.cssForIndex(item)} className={this.cssForIndex(item)}
onClick={() => this.props.selector(item)} onClick={() => this.props.selector(item)}>
key={inx}> {item}
{item} </button>)}
</button>
)
}
</div>; </div>;
} }
} }

View File

@ -5,7 +5,7 @@ import { bot } from "../../../__test_support__/fake_state/bot";
import { PeripheralsProps } from "../../../devices/interfaces"; import { PeripheralsProps } from "../../../devices/interfaces";
import { fakePeripheral } from "../../../__test_support__/fake_state/resources"; import { fakePeripheral } from "../../../__test_support__/fake_state/resources";
import { clickButton } from "../../../__test_support__/helpers"; import { clickButton } from "../../../__test_support__/helpers";
import { SpecialStatus } from "farmbot"; import { SpecialStatus, FirmwareHardware } from "farmbot";
import { error } from "../../../toast/toast"; import { error } from "../../../toast/toast";
describe("<Peripherals />", () => { describe("<Peripherals />", () => {
@ -14,7 +14,8 @@ describe("<Peripherals />", () => {
bot, bot,
peripherals: [fakePeripheral()], peripherals: [fakePeripheral()],
dispatch: jest.fn(), dispatch: jest.fn(),
disabled: false disabled: false,
firmwareHardware: undefined,
}; };
} }
@ -73,11 +74,28 @@ describe("<Peripherals />", () => {
expect(p.dispatch).toHaveBeenCalled(); expect(p.dispatch).toHaveBeenCalled();
}); });
it("adds farmduino peripherals", () => { it.each<[FirmwareHardware, number]>([
["arduino", 2],
["farmduino", 5],
["farmduino_k14", 5],
["farmduino_k15", 5],
["express_k10", 3],
])("adds peripherals: %s", (firmware, expectedAdds) => {
const p = fakeProps(); const p = fakeProps();
p.firmwareHardware = firmware;
const wrapper = mount(<Peripherals {...p} />); const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true }); wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "farmduino"); clickButton(wrapper, 3, "stock");
expect(p.dispatch).toHaveBeenCalledTimes(5); expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
});
it("hides stock button", () => {
const p = fakeProps();
p.firmwareHardware = "none";
const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true });
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
}); });
}); });

View File

@ -10,7 +10,7 @@ import { mount } from "enzyme";
import { PeripheralList } from "../peripheral_list"; import { PeripheralList } from "../peripheral_list";
import { import {
TaggedPeripheral, TaggedPeripheral,
SpecialStatus SpecialStatus,
} from "farmbot"; } from "farmbot";
import { Pins } from "farmbot/dist"; import { Pins } from "farmbot/dist";

View File

@ -51,31 +51,52 @@ export class Peripherals
newPeripheral = ( newPeripheral = (
pin: number | undefined = undefined, pin: number | undefined = undefined,
label = t("New Peripheral") label = t("New Peripheral"),
) => { ) => {
this.props.dispatch(init("Peripheral", { pin, label })); this.props.dispatch(init("Peripheral", { pin, label }));
}; };
farmduinoPeripherals = () => { get stockPeripherals() {
this.newPeripheral(7, t("Lighting")); switch (this.props.firmwareHardware) {
this.newPeripheral(8, t("Water")); case "arduino":
this.newPeripheral(9, t("Vacuum")); return [
this.newPeripheral(10, t("Peripheral ") + "4"); { pin: 8, label: t("Water") },
this.newPeripheral(12, t("Peripheral ") + "5"); { pin: 9, label: t("Vacuum") },
];
case "farmduino":
case "farmduino_k14":
case "farmduino_k15":
default:
return [
{ pin: 7, label: t("Lighting") },
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
{ pin: 10, label: t("Peripheral ") + "4" },
{ pin: 12, label: t("Peripheral ") + "5" },
];
case "express_k10":
return [
{ pin: 7, label: t("Lighting") },
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
];
}
} }
render() { render() {
const { isEditing } = this.state; const { isEditing } = this.state;
const status = getArrayStatus(this.props.peripherals); const status = getArrayStatus(this.props.peripherals);
const editButtonText = isEditing
? t("Back")
: t("Edit");
return <Widget className="peripherals-widget"> return <Widget className="peripherals-widget">
<WidgetHeader title={t("Peripherals")} helpText={ToolTips.PERIPHERALS}> <WidgetHeader title={t("Peripherals")} helpText={ToolTips.PERIPHERALS}>
<button <button
className="fb-button gray" className="fb-button gray"
onClick={this.toggle} onClick={this.toggle}
title={editButtonText}
disabled={!!status && isEditing}> disabled={!!status && isEditing}>
{!isEditing && t("Edit")} {editButtonText}
{isEditing && t("Back")}
</button> </button>
<SaveBtn <SaveBtn
hidden={!isEditing} hidden={!isEditing}
@ -85,17 +106,20 @@ export class Peripherals
hidden={!isEditing} hidden={!isEditing}
className="fb-button green" className="fb-button green"
type="button" type="button"
title={t("add peripheral")}
onClick={() => this.newPeripheral()}> onClick={() => this.newPeripheral()}>
<i className="fa fa-plus" /> <i className="fa fa-plus" />
</button> </button>
<button <button
hidden={!isEditing} hidden={!isEditing || this.props.firmwareHardware == "none"}
className="fb-button green" className="fb-button green"
type="button" type="button"
onClick={this.farmduinoPeripherals}> title={t("add stock peripherals")}
onClick={() => this.stockPeripherals.map(p =>
this.newPeripheral(p.pin, p.label))}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} /> <i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
Farmduino {t("Stock")}
</button> </button>
</WidgetHeader> </WidgetHeader>
<WidgetBody> <WidgetBody>
{this.showPins()} {this.showPins()}

View File

@ -26,6 +26,5 @@ export const PeripheralForm = (props: PeripheralFormProps) =>
dispatch={props.dispatch} dispatch={props.dispatch}
uuid={peripheral.uuid} /> uuid={peripheral.uuid} />
</Col> </Col>
</Row> </Row>)}
)}
</div>; </div>;

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