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 "tzinfo" # For validation of user selected timezone names
gem "valid_url"
# gem "farady", "~> 1.0.0"
gem "kaminari"
group :development, :test do
gem "climate_control"

View File

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

View File

@ -80,6 +80,16 @@ module Api
{ root: false, user: current_user }
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
def clean_expired_farm_events

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ class ToolSlot < Point
MIN_PULLOUT = PULLOUT_DIRECTIONS.min
PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\
"%{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"\
" before reassigning."

View File

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

View File

@ -27,7 +27,7 @@ module Devices
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0,
y: 25,
z: -200,
z: 0,
tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
@ -37,25 +37,18 @@ module Devices
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0,
y: 50,
z: -200,
z: 0,
tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_3
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_3; end
def tool_slots_slot_4; end
def tool_slots_slot_5; 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_tray; end
@ -69,11 +62,6 @@ module Devices
add_tool(ToolNames::SEED_TROUGH_2)
end
def tools_seed_trough_3
@tools_seed_trough_3 ||=
add_tool(ToolNames::SEED_TROUGH_3)
end
def tools_seeder; end
def tools_soil_sensor; end
def tools_watering_nozzle; end

View File

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

View File

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

View File

@ -31,7 +31,6 @@ module Devices
LIGHTING = "Lighting"
SEED_TROUGH_1 = "Seed Trough 1"
SEED_TROUGH_2 = "Seed Trough 2"
SEED_TROUGH_3 = "Seed Trough 3"
end
# 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_5; 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_tray; end
def tools_seed_trough_1; end
def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder; end
def tools_soil_sensor; end
def tools_watering_nozzle; end

View File

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

View File

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

View File

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

View File

@ -4,9 +4,15 @@ export const panelState = (): ControlPanelState => {
return {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: 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 { panelState } from "../control_panel_state";
export const bot: Everything["bot"] = {
"consistent": true,
"stepSize": 100,
"controlPanelState": {
"homing_and_calibration": false,
"motors": false,
"encoders_and_endstops": false,
"danger_zone": false,
"power_and_reset": false,
"pin_guard": false,
},
"controlPanelState": panelState(),
"hardware": {
"gpio_registry": {},
"mcu_params": {

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@ const tr0: TaggedResource = {
},
"speed": 100
}
}
},
],
"args": {
"version": 4,
@ -287,7 +287,7 @@ const tr12: TaggedResource = {
"regimen_id": 11,
"sequence_id": 23,
"time_offset": 345900000
}
},
],
body: [],
},
@ -345,7 +345,7 @@ export const FAKE_RESOURCES: TaggedResource[] = [
tr0,
tr14,
tr15,
log
log,
];
const KIND: keyof TaggedResource = "kind"; // Safety first, kids.
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 { bot } from "../__test_support__/fake_state/bot";
import {
fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv
fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv,
} from "../__test_support__/fake_state/resources";
import { fakeState } from "../__test_support__/fake_state";
import {
buildResourceIndex
buildResourceIndex,
} from "../__test_support__/resource_index_builder";
import { ResourceName } from "farmbot";
import { fakeTimeSettings } from "../__test_support__/fake_time_settings";
@ -125,7 +125,7 @@ describe("<App />: NavBar", () => {
"Device",
"Sequences",
"Regimens",
"Farmware"
"Farmware",
];
strings.map(string => expect(t).toContain(string));
wrapper.unmount();
@ -157,7 +157,6 @@ describe("mapStateToProps()", () => {
const state = fakeState();
const config = fakeFbosConfig();
config.body.auto_sync = true;
config.body.api_migrated = true;
const fakeEnv = fakeFarmwareEnv();
state.resources = buildResourceIndex([config, fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };

View File

@ -1,17 +1,16 @@
jest.mock("../util", () => {
return {
attachToRoot: jest.fn(),
// Incidental mock. Can be removed if errors go away.
trim: jest.fn(x => x)
};
});
jest.mock("../util", () => ({
attachToRoot: jest.fn(),
// Incidental mock. Can be removed if errors go away.
trim: jest.fn(x => x),
urlFriendly: jest.fn(),
}));
jest.mock("../redux/store", () => {
return { store: { dispatch: jest.fn() } };
});
jest.mock("../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => false, }
DevSettings: { futureFeaturesEnabled: () => false }
}));
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 {
responseFulfilled, isLocalRequest, requestFulfilled, responseRejected
responseFulfilled, isLocalRequest, requestFulfilled, responseRejected,
} from "../interceptors";
import { AxiosResponse, Method } from "axios";
import { uuid } from "farmbot";

View File

@ -30,7 +30,6 @@ import "../regimens/editor/interfaces";
import "../regimens/interfaces";
import "../resources/interfaces";
import "../sequences/interfaces";
import "../tools/interfaces";
describe("interfaces", () => {
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 { API } from "../api/index";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { connect } from "react-redux";
import {
Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget
Settings, ChangePassword, ExportAccountPanel, DangerousDeleteWidget,
} from "./components";
import { Props } from "./interfaces";
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 });
onChange = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget;
if (isKey(name)) {
this.tempHack(name);
this.props.dispatch(edit(this.props.user, { [name]: value }));
const { value } = e.currentTarget;
const field = e.currentTarget.name;
if (isKey(field)) {
this.tempHack(field);
this.props.dispatch(edit(this.props.user, { [field]: value }));
} else {
throw new Error("Bad key: " + name);
throw new Error("Bad key: " + field);
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import * as React from "react";
import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
const OUTER_STYLE: React.CSSProperties = {
borderRadius: "10px",
@ -47,7 +48,7 @@ export function Apology(_: {}) {
<li>
<span>
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
to the error) helps us identify solutions more quickly.
</span>

View File

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

View File

@ -1,7 +1,7 @@
import axios from "axios";
import {
fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL,
fetchLatestGHBetaRelease
fetchReleases, fetchMinOsFeatureData,
fetchLatestGHBetaRelease,
} from "../devices/actions";
import { AuthState } from "./interfaces";
import { ReduxAction } from "../redux/interfaces";
@ -10,12 +10,13 @@ import { API } from "../api";
import {
responseFulfilled,
responseRejected,
requestFulfilled
requestFulfilled,
} from "../interceptors";
import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device";
import { getFirstPartyFarmwareList } from "../farmware/actions";
import { readOnlyInterceptor } from "../read_only_mode";
import { ExternalUrl } from "../external_urls";
export function didLogin(authState: AuthState, dispatch: Function) {
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" &&
dispatch(fetchLatestGHBetaRelease(beta_os_update_server));
dispatch(getFirstPartyFarmwareList());
dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL));
dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions));
dispatch(setToken(authState));
Sync.fetchSyncData(dispatch);
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", () => ({
Session: {
fetchStoredToken: jest.fn(),
getAll: () => undefined,
clear: jest.fn()
clear: jest.fn(),
}
}));
jest.mock("../../auth/actions", () => ({
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 { setToken, didLogin } from "../../auth/actions";
import { Session } from "../../session";
import { auth } from "../../__test_support__/fake_state/token";
import { fakeState } from "../../__test_support__/fake_state";
describe("Actions", () => {
it("calls didLogin()", () => {
jest.resetAllMocks();
describe("ready()", () => {
it("uses new token", async () => {
const fakeAuth = { token: "fake token data" };
mockTimeout = Promise.resolve(fakeAuth);
const dispatch = jest.fn();
const thunk = ready();
thunk(dispatch, fakeState);
expect(setToken).toHaveBeenCalled();
const state = fakeState();
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", () => {
jest.resetAllMocks();
it("uses old token", async () => {
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 state = fakeState();
delete state.auth;
const getState = () => state;
const thunk = ready();
console.warn = jest.fn();
thunk(dispatch, getState);
expect(setToken).not.toHaveBeenCalled();
expect(didLogin).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
expect(Session.clear).toHaveBeenCalled();
});
});
describe("storeToken()", () => {
it("stores token", () => {
const old = auth;
old.token.unencoded.jti = "old";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,15 @@
jest.mock("../../slow_down", () => {
return {
slowDown: jest.fn((fn: Function) => fn),
};
});
jest.mock("../../../devices/actions", () => ({
badVersion: jest.fn(),
EXPECTED_MAJOR: 1,
EXPECTED_MINOR: 0,
jest.mock("../../slow_down", () => ({
slowDown: jest.fn((fn: Function) => fn)
}));
jest.mock("../../../devices/actions", () => ({ badVersion: jest.fn() }));
import {
onStatus,
incomingStatus,
incomingLegacyStatus,
onLegacyStatus,
HACKY_FLAGS
HACKY_FLAGS,
} from "../../connect_device";
import { slowDown } from "../../slow_down";
import { fakeState } from "../../../__test_support__/fake_state";
@ -49,8 +43,10 @@ describe("onStatus()", () => {
});
it("version ok", () => {
globalConfig.MINIMUM_FBOS_VERSION = "1.0.0";
callOnStatus("1.0.0");
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 { Actions } from "../../constants";
import {
startTracking, outstandingRequests, stopTracking, cleanUUID
startTracking, outstandingRequests, stopTracking, cleanUUID,
} from "../data_consistency";
const unprocessedUuid = "~UU.ID~";

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { TaggedResource, SpecialStatus } from "farmbot";
import { overwrite, init } from "../api/crud";
import { handleInbound } from "./auto_sync_handle_inbound";
import {
SyncPayload, MqttDataResult, Reason, UpdateMqttData
SyncPayload, MqttDataResult, Reason, UpdateMqttData,
} from "./interfaces";
import { outstandingRequests } from "./data_consistency";
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 { GetState, ReduxAction } from "../redux/interfaces";
import { Content, Actions } from "../constants";
import {
EXPECTED_MAJOR,
EXPECTED_MINOR,
commandOK,
badVersion,
commandErr
} from "../devices/actions";
import { commandOK, badVersion, commandErr } from "../devices/actions";
import { init } from "../api/crud";
import { AuthState } from "../auth/interfaces";
import { autoSync } from "./auto_sync";
@ -123,7 +117,7 @@ const setBothUp = () => bothUp();
const legacyChecks = (getState: GetState) => {
const { controller_version } = getState().bot.hardware.informational_settings;
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(); }
HACKY_FLAGS.needVersionCheck = false;
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { generateReducer } from "../redux/generate_reducer";
import { Actions } from "../constants";
import {
ConnectionState,
EdgeStatus
EdgeStatus,
} from "./interfaces";
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.`);
export const PIN_BINDINGS =
trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is
activated.`);
trim(`Assign an action or sequence to execute when a Raspberry Pi
GPIO pin is activated.`);
export const PIN_BINDING_WARNING =
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.`);
// Hardware Settings: Homing and Calibration
export const HOMING =
export const HOMING_ENCODERS =
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
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 =
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
when the device powers on.
Warning! This will perform homing on all axes when the
device powers on. Encoders or endstops must be enabled.
when the device powers on. Warning! This will perform homing on all
axes when the device powers on. Encoders or endstops must be enabled.
It is recommended to make sure homing works properly before enabling
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 =
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.
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
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 =
trim(`Maximum travel speed after acceleration in millimeters per second.
(default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`);
@ -132,18 +135,22 @@ export namespace ToolTips {
export const MOTOR_CURRENT =
trim(`Motor current in milliamps. (default: 600)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENABLE_X2_MOTOR =
trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS.
(default: enabled)`);
// Hardware Settings: Encoders and Endstops
// Hardware Settings: Encoders / Stall Detection
export const ENABLE_ENCODERS =
trim(`Enable use of rotary encoders for stall detection,
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 =
trim(`Use encoders for positioning. (default: disabled)`);
@ -151,17 +158,22 @@ export namespace ToolTips {
trim(`Reverse the direction of encoder position reading.
(default: disabled)`);
export const MAX_MISSED_STEPS =
export const MAX_MISSED_STEPS_ENCODERS =
trim(`Number of steps missed (determined by encoder) before motor is
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)`);
export const ENCODER_SCALING =
trim(`encoder scaling factor = 10000 * (motor resolution * microsteps)
/ (encoder resolution). (default: 5556 (10000*200/360))`);
// Hardware Settings: Endstops
export const ENABLE_ENDSTOPS =
trim(`Enable use of electronic end-stops for end detection,
calibration and homing. (default: disabled)`);
@ -173,6 +185,18 @@ export namespace ToolTips {
trim(`Invert axis end-stops. Enable for normally closed (NC),
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
export const PIN_GUARD_PIN_NUMBER =
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 =
trim(`The Find Home step instructs the device to perform a homing
command (using encoders or endstops) to find and set zero for
the chosen axis or axes.`);
command (using encoders, stall detection, or endstops) to find and set
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 =
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.`);
export const OS_AUTO_UPDATE =
trim(`When enabled, FarmBot OS will periodically check for, download,
and install updates automatically.`);
trim(`When enabled, FarmBot OS will automatically download and install
software updates at the chosen time.`);
export const AUTO_SYNC =
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.`);
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?`);
export const DIAGNOSTIC_CHECK =
@ -674,9 +702,9 @@ export namespace Content {
trim(`FarmBot sent a malformed message. You may need to upgrade
FarmBot OS. Please upgrade FarmBot OS and log back in.`);
export const OLD_FBOS_REC_UPGRADE = trim(`Your version of FarmBot OS is
outdated and will soon no longer be supported. Please update your device as
soon as possible.`);
export const OLD_FBOS_REC_UPGRADE =
trim(`Your version of FarmBot OS is outdated and will soon no longer
be supported. Please update your device as soon as possible.`);
export const EXPERIMENTAL_WARNING =
trim(`Warning! This is an EXPERIMENTAL feature. This feature may be
@ -715,8 +743,8 @@ export namespace Content {
export const END_DETECTION_DISABLED =
trim(`This command will not execute correctly because you do not have
encoders or endstops enabled for the chosen axis. Enable endstops or
encoders from the Device page for: `);
encoders, stall detection, or endstops enabled for the chosen axis.
Enable endstops, encoders, or stall detection from the Device page for: `);
export const IN_USE =
trim(`Used in another resource. Protected from deletion.`);
@ -784,7 +812,10 @@ export namespace Content {
trim(`add this crop on OpenFarm?`);
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 =
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.`);
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 =
trim(`Add the newly created tools and seed containers to the
corresponding tool slots on FarmBot:
press edit and then + to create a tool slot.`);
corresponding slots on FarmBot:
press the + button to create a slot.`);
export const 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.`);
}
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 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.
This usually happens because of poor WiFi connectivity in the garden,
a bad password during configuration, a very long power outage, or
blocked ports on FarmBot's local network. Please refer IT staff to
https://software.farm.bot/docs/for-it-security-professionals`);
blocked ports on FarmBot's local network. Please refer IT staff to:`);
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { bot } from "../../../__test_support__/fake_state/bot";
import { PeripheralsProps } from "../../../devices/interfaces";
import { fakePeripheral } from "../../../__test_support__/fake_state/resources";
import { clickButton } from "../../../__test_support__/helpers";
import { SpecialStatus } from "farmbot";
import { SpecialStatus, FirmwareHardware } from "farmbot";
import { error } from "../../../toast/toast";
describe("<Peripherals />", () => {
@ -14,7 +14,8 @@ describe("<Peripherals />", () => {
bot,
peripherals: [fakePeripheral()],
dispatch: jest.fn(),
disabled: false
disabled: false,
firmwareHardware: undefined,
};
}
@ -73,11 +74,28 @@ describe("<Peripherals />", () => {
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();
p.firmwareHardware = firmware;
const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "farmduino");
expect(p.dispatch).toHaveBeenCalledTimes(5);
clickButton(wrapper, 3, "stock");
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 {
TaggedPeripheral,
SpecialStatus
SpecialStatus,
} from "farmbot";
import { Pins } from "farmbot/dist";

View File

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

View File

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

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