Merge pull request #1701 from FarmBot/staging

v9.1.3 - Jolly Juniper
hmm
Rick Carlino 2020-02-20 12:57:33 -06:00 committed by GitHub
commit a5b1d5631e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
188 changed files with 2998 additions and 1746 deletions

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

@ -6,6 +6,36 @@ module Devices
.fbos_config .fbos_config
.update!(firmware_hardware: FbosConfig::FARMDUINO_K15) .update!(firmware_hardware: FbosConfig::FARMDUINO_K15)
end 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 end
end end

View File

@ -18,6 +18,36 @@ module Devices
def settings_default_map_size_y def settings_default_map_size_y
device.web_app_config.update!(map_size_y: 2_900) device.web_app_config.update!(map_size_y: 2_900)
end 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 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

@ -1,8 +1,8 @@
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 because the following sequences are " \
"still using it: %s" "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 because it is still in a tool slot. " \
"Please remove it from the tool slot first." "Please remove it from the tool slot first."
required do required do
@ -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,7 +4,10 @@ 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

View File

@ -4,12 +4,15 @@ export const bot: Everything["bot"] = {
"consistent": true, "consistent": true,
"stepSize": 100, "stepSize": 100,
"controlPanelState": { "controlPanelState": {
"homing_and_calibration": false, homing_and_calibration: false,
"motors": false, motors: false,
"encoders_and_endstops": false, encoders: false,
"danger_zone": false, endstops: false,
"power_and_reset": false, error_handling: false,
"pin_guard": false, pin_bindings: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false,
}, },
"hardware": { "hardware": {
"gpio_registry": {}, "gpio_registry": {},

View File

@ -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,10 +1,9 @@
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() } };

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

@ -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

@ -1,6 +1,6 @@
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";
@ -16,6 +16,7 @@ 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,15 +1,9 @@
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,
@ -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

@ -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

@ -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
@ -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
tool slots for them to by pressing the tool 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 seed container 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 tool slots on FarmBot:
press edit and then + to create a tool slot.`); press the + button to create a tool 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,87 @@ 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 = `dangerZone`,
resetHardwareParams = `Reset hardware parameter defaults`,
// Pin Bindings
pinBindings = `Pin Bindings`,
// FarmBot OS
name = `name`,
timezone = `timezone`,
camera = `camera`,
firmware = `firmware`,
farmbotOSAutoUpdate = `Farmbot OS 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`,
}
export namespace DiagnosticMessages { export namespace DiagnosticMessages {
export const OK = trim(`All systems nominal.`); export const OK = trim(`All systems nominal.`);
@ -924,8 +1047,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

@ -38,6 +38,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 +51,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

@ -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,18 @@ 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);
}); });
}); });

View File

@ -56,12 +56,31 @@ export class Peripherals
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() {
@ -92,10 +111,11 @@ export class Peripherals
hidden={!isEditing} hidden={!isEditing}
className="fb-button green" className="fb-button green"
type="button" type="button"
onClick={this.farmduinoPeripherals}> 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

@ -18,7 +18,8 @@ describe("<Sensors />", () => {
bot, bot,
sensors: [fakeSensor1, fakeSensor2], sensors: [fakeSensor1, fakeSensor2],
dispatch: jest.fn(), dispatch: jest.fn(),
disabled: false disabled: false,
firmwareHardware: undefined,
}; };
} }
@ -68,8 +69,16 @@ describe("<Sensors />", () => {
it("adds stock sensors", () => { it("adds stock sensors", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = mount(<Sensors {...p} />); const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
wrapper.setState({ isEditing: true }); wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "stock sensors"); clickButton(wrapper, 3, "stock sensors");
expect(p.dispatch).toHaveBeenCalledTimes(2); expect(p.dispatch).toHaveBeenCalledTimes(2);
}); });
it("doesn't display + stock button", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
});
}); });

View File

@ -100,4 +100,11 @@ describe("<SensorList/>", function () {
readSensorBtn.last().simulate("click"); readSensorBtn.last().simulate("click");
expect(mockDevice.readPin).not.toHaveBeenCalled(); expect(mockDevice.readPin).not.toHaveBeenCalled();
}); });
it("renders analog reading", () => {
const p = fakeProps();
p.pins[50] && (p.pins[50].value = 600);
const wrapper = mount(<SensorList {...p} />);
expect(wrapper.html()).toContain("margin-left: -3.5rem");
});
}); });

View File

@ -10,6 +10,7 @@ import { saveAll, init } from "../../api/crud";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { uniq } from "lodash"; import { uniq } from "lodash";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
export class Sensors extends React.Component<SensorsProps, SensorState> { export class Sensors extends React.Component<SensorsProps, SensorState> {
constructor(props: SensorsProps) { constructor(props: SensorsProps) {
@ -79,14 +80,15 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
onClick={() => this.newSensor()}> onClick={() => this.newSensor()}>
<i className="fa fa-plus" /> <i className="fa fa-plus" />
</button> </button>
<button {!isExpressBoard(this.props.firmwareHardware) &&
hidden={!isEditing} <button
className="fb-button green" hidden={!isEditing}
type="button" className="fb-button green"
onClick={this.stockSensors}> type="button"
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} /> onClick={this.stockSensors}>
{t("Stock sensors")} <i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
</button> {t("Stock sensors")}
</button>}
</WidgetHeader> </WidgetHeader>
<WidgetBody> <WidgetBody>
{this.showPins()} {this.showPins()}

View File

@ -2,6 +2,7 @@ import * as React from "react";
import { get } from "lodash"; import { get } from "lodash";
import { Page } from "./ui/index"; import { Page } from "./ui/index";
import { Session } from "./session"; import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
/** Use currying to pass down `error` object for now. */ /** Use currying to pass down `error` object for now. */
export function crashPage(error: object) { export function crashPage(error: object) {
@ -24,7 +25,7 @@ export function crashPage(error: object) {
<li>Perform a "hard refresh" (<strong>CTRL + SHIFT + R</strong> on most machines).</li> <li>Perform a "hard refresh" (<strong>CTRL + SHIFT + R</strong> on most machines).</li>
<li><span><a onClick={() => Session.clear()}>Log out by clicking here.</a></span></li> <li><span><a onClick={() => Session.clear()}>Log out by clicking here.</a></span></li>
<li>Send the error information (below) to our developer team via the <li>Send the error information (below) to our developer team via the
<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) help us identify solutions more quickly. </li> to the error) help us identify solutions more quickly. </li>
</ol> </ol>

View File

@ -1,5 +1,6 @@
// Padding for the popups. // Padding for the popups.
.bp3-popover-content { .bp3-popover-content {
z-index: 999;
padding: 1rem; padding: 1rem;
} }

View File

@ -580,6 +580,7 @@
font-size: 1.4rem; font-size: 1.4rem;
} }
} }
.tools-header,
.tool-slots-header { .tool-slots-header {
display: flex; display: flex;
margin-top: 1rem; margin-top: 1rem;
@ -624,6 +625,12 @@
} }
} }
.add-stock-tools { .add-stock-tools {
.filter-search {
margin-bottom: 1rem;
button {
margin-top: 0.2rem;
}
}
ul { ul {
font-size: 1.2rem; font-size: 1.2rem;
padding-left: 1rem; padding-left: 1rem;

View File

@ -433,14 +433,17 @@ a {
} }
} }
.pin-bindings-widget { .pin-bindings {
.fa-exclamation-triangle { .fa-exclamation-triangle {
color: $orange; color: $orange;
margin-left: 1rem;
margin-top: 0.75rem;
} }
.fa-th-large { .fa-th-large {
position: absolute;
top: 0.75rem;
left: 0.5rem;
color: $dark_gray; color: $dark_gray;
margin-top: 0.5rem;
margin-left: 0.5rem;
} }
.fb-button { .fb-button {
&.green { &.green {
@ -449,16 +452,27 @@ a {
} }
.bindings-list { .bindings-list {
margin-bottom: 1rem; margin-bottom: 1rem;
margin-left: 1rem;
font-size: 1.2rem; font-size: 1.2rem;
} }
.binding-type-dropdown {
margin-bottom: 1.5rem;
}
.stock-pin-bindings-button { .stock-pin-bindings-button {
button { button {
margin: 0 !important; margin: 1rem;
float: left;
margin-left: 2rem;
} }
i { i {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
} }
.bp3-popover-wrapper {
display: inline;
float: none !important;
margin-left: 1rem;
}
} }
.sensor-history-widget { .sensor-history-widget {
@ -1308,6 +1322,12 @@ ul {
} }
} }
.boolean-camera-calibration-config {
input[type=checkbox] {
display: block;
}
}
.tour-list { .tour-list {
margin: auto; margin: auto;
max-width: 300px; max-width: 300px;
@ -1519,16 +1539,24 @@ textarea:focus {
box-shadow: 0 0 10px rgba(0,0,0,.2); box-shadow: 0 0 10px rgba(0,0,0,.2);
} }
.sort-path-info-bar { .sort-option-bar {
background: lightgray;
cursor: pointer; cursor: pointer;
font-size: 1.1rem;
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
white-space: nowrap; border: 2px solid $panel_light_blue;
line-height: 1.75rem; &:hover, &.selected {
&:hover { border: 2px solid $medium_gray;
background: darken(lightgray, 10%); border-radius: 2px;
.sort-path-info-bar {
background: darken($light_gray, 10%);
}
}
.sort-path-info-bar {
background: $light_gray;
font-size: 1.2rem;
padding-left: 0.5rem;
white-space: nowrap;
line-height: 2.5rem;
} }
} }
@ -1601,3 +1629,23 @@ textarea:focus {
} }
} }
} }
.section {
display: block !important;
}
.highlight,
.unhighlight {
display: flex;
}
.highlight {
background-color: $light_yellow;
box-shadow: 0px 0px 7px 4px $light_yellow;
}
.unhighlight {
transition: background-color 10s linear, box-shadow 10s linear;
background-color: transparent;
box-shadow: none;
}

View File

@ -97,6 +97,7 @@
} }
} }
.camera-calibration,
.weed-detector{ .weed-detector{
.farmware-button{ .farmware-button{
position: relative; position: relative;

View File

@ -2,16 +2,13 @@ import { connect, MqttClient } from "mqtt";
import React from "react"; import React from "react";
import { uuid } from "farmbot"; import { uuid } from "farmbot";
import axios from "axios"; import axios from "axios";
import { ExternalUrl } from "../external_urls";
interface State { interface State {
error: Error | undefined; error: Error | undefined;
stage: string; stage: string;
} }
const VIDEO_URL =
"https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018";
const PHONE_URL =
"https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097";
const WS_CONFIG = { const WS_CONFIG = {
username: "farmbot_demo", username: "farmbot_demo",
password: "required, but not used.", password: "required, but not used.",
@ -63,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> {
return <div className="demo-container"> return <div className="demo-container">
<video muted={true} autoPlay={true} loop={true} className="demo-video"> <video muted={true} autoPlay={true} loop={true} className="demo-video">
<source src={VIDEO_URL} type="video/mp4" /> <source src={ExternalUrl.Video.desktop} type="video/mp4" />
</video> </video>
<img className="demo-phone" src={PHONE_URL} /> <img className="demo-phone" src={ExternalUrl.Video.mobile} />
<button className="demo-button" onClick={this.requestAccount}> <button className="demo-button" onClick={this.requestAccount}>
{this.state.stage} {this.state.stage}
</button> </button>

View File

@ -307,7 +307,7 @@ describe("commandErr()", () => {
}); });
}); });
describe("toggleControlPanel()", function () { describe("toggleControlPanel()", () => {
it("toggles", () => { it("toggles", () => {
const action = actions.toggleControlPanel("homing_and_calibration"); const action = actions.toggleControlPanel("homing_and_calibration");
expect(action.payload).toEqual("homing_and_calibration"); expect(action.payload).toEqual("homing_and_calibration");

View File

@ -35,7 +35,6 @@ describe("mapStateToProps()", () => {
it("uses the API as the source of FBOS settings", () => { it("uses the API as the source of FBOS settings", () => {
const fakeApiConfig = fakeFbosConfig(); const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.auto_sync = true; fakeApiConfig.body.auto_sync = true;
fakeApiConfig.body.api_migrated = true;
mockFbosConfig = fakeApiConfig; mockFbosConfig = fakeApiConfig;
const props = mapStateToProps(fakeState()); const props = mapStateToProps(fakeState());
expect(props.sourceFbosConfig("auto_sync")).toEqual({ expect(props.sourceFbosConfig("auto_sync")).toEqual({
@ -53,19 +52,6 @@ describe("mapStateToProps()", () => {
}); });
}); });
it("uses the bot as the source of FBOS settings: ignore API defaults", () => {
const state = fakeState();
state.bot.hardware.configuration.auto_sync = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.auto_sync = true;
fakeApiConfig.body.api_migrated = false;
mockFbosConfig = fakeApiConfig;
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("auto_sync")).toEqual({
value: false, consistent: true
});
});
it("returns API Farmware env vars", () => { it("returns API Farmware env vars", () => {
const state = fakeState(); const state = fakeState();
state.bot.hardware.user_env = {}; state.bot.hardware.user_env = {};

View File

@ -26,11 +26,6 @@ import { t } from "../i18next_wrapper";
const ON = 1, OFF = 0; const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams; export type ConfigKey = keyof McuParams;
export const EXPECTED_MAJOR = 6;
export const EXPECTED_MINOR = 0;
export const FEATURE_MIN_VERSIONS_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
"FEATURE_MIN_VERSIONS.json";
// Already filtering messages in FarmBot OS and the API- this is just for // Already filtering messages in FarmBot OS and the API- this is just for
// an additional layer of safety. // an additional layer of safety.
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"]; const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];
@ -132,7 +127,7 @@ export function sync(): Thunk {
return function (_dispatch, getState) { return function (_dispatch, getState) {
const currentFBOSversion = const currentFBOSversion =
getState().bot.hardware.informational_settings.controller_version; getState().bot.hardware.informational_settings.controller_version;
const IS_OK = versionOK(currentFBOSversion, EXPECTED_MAJOR, EXPECTED_MINOR); const IS_OK = versionOK(currentFBOSversion);
if (IS_OK) { if (IS_OK) {
getDevice() getDevice()
.sync() .sync()

View File

@ -7,13 +7,14 @@ import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions"; import { settingToggle } from "../../actions";
import { bot } from "../../../__test_support__/fake_state/bot"; import { bot } from "../../../__test_support__/fake_state/bot";
import { BooleanMCUInputGroupProps } from "../interfaces"; import { BooleanMCUInputGroupProps } from "../interfaces";
import { DeviceSetting } from "../../../constants";
describe("BooleanMCUInputGroup", () => { describe("BooleanMCUInputGroup", () => {
const fakeProps = (): BooleanMCUInputGroupProps => ({ const fakeProps = (): BooleanMCUInputGroupProps => ({
sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }), sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }),
dispatch: jest.fn(), dispatch: jest.fn(),
tooltip: "Tooltip", tooltip: "Tooltip",
name: "Name", label: DeviceSetting.invertEncoders,
x: "encoder_invert_x", x: "encoder_invert_x",
y: "encoder_invert_y", y: "encoder_invert_y",
z: "encoder_invert_z", z: "encoder_invert_z",

View File

@ -22,6 +22,8 @@ import axios from "axios";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { edit } from "../../../api/crud"; import { edit } from "../../../api/crud";
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources"; import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
import { formEvent } from "../../../__test_support__/fake_html_events";
import { Content } from "../../../constants";
describe("<FarmbotOsSettings />", () => { describe("<FarmbotOsSettings />", () => {
beforeEach(() => { beforeEach(() => {
@ -54,8 +56,8 @@ describe("<FarmbotOsSettings />", () => {
const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />); const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />);
expect(osSettings.find("input").length).toBe(1); expect(osSettings.find("input").length).toBe(1);
expect(osSettings.find("button").length).toBe(7); expect(osSettings.find("button").length).toBe(7);
["NAME", "TIME ZONE", "FARMBOT OS", "CAMERA", "FIRMWARE"] ["name", "time zone", "farmbot os", "camera", "firmware"]
.map(string => expect(osSettings.text()).toContain(string)); .map(string => expect(osSettings.text().toLowerCase()).toContain(string));
}); });
it("fetches OS release notes", async () => { it("fetches OS release notes", async () => {
@ -115,4 +117,18 @@ describe("<FarmbotOsSettings />", () => {
const osSettings = shallow(<FarmbotOsSettings {...p} />); const osSettings = shallow(<FarmbotOsSettings {...p} />);
expect(osSettings.find("BootSequenceSelector").length).toEqual(1); expect(osSettings.find("BootSequenceSelector").length).toEqual(1);
}); });
it("prevents default form submit action", () => {
const osSettings = shallow(<FarmbotOsSettings {...fakeProps()} />);
const e = formEvent();
osSettings.find("form").simulate("submit", e);
expect(e.preventDefault).toHaveBeenCalled();
});
it("warns about timezone mismatch", () => {
const p = fakeProps();
p.deviceAccount.body.timezone = "different";
const osSettings = mount(<FarmbotOsSettings {...p} />);
expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING);
});
}); });

View File

@ -1,4 +1,5 @@
import { boardType } from "../firmware_hardware_support"; import { boardType, getFwHardwareValue } from "../firmware_hardware_support";
import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources";
describe("boardType()", () => { describe("boardType()", () => {
it("returns Farmduino", () => { it("returns Farmduino", () => {
@ -32,3 +33,18 @@ describe("boardType()", () => {
expect(boardType("none")).toEqual("none"); expect(boardType("none")).toEqual("none");
}); });
}); });
describe("getFwHardwareValue()", () => {
it("returns undefined", () => {
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "wrong";
expect(getFwHardwareValue(fbosConfig)).toEqual(undefined);
expect(getFwHardwareValue(undefined)).toEqual(undefined);
});
it("returns real value", () => {
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "express_k10";
expect(getFwHardwareValue(fbosConfig)).toEqual("express_k10");
});
});

View File

@ -65,13 +65,7 @@ describe("<HardwareSettings />", () => {
it("shows param export menu", () => { it("shows param export menu", () => {
const p = fakeProps(); const p = fakeProps();
p.firmwareConfig = fakeFirmwareConfig().body; p.firmwareConfig = fakeFirmwareConfig().body;
p.firmwareConfig.api_migrated = true;
const wrapper = shallow(<HardwareSettings {...p} />); const wrapper = shallow(<HardwareSettings {...p} />);
expect(wrapper.html()).toContain("fa-download"); expect(wrapper.html()).toContain("fa-download");
}); });
it("doesn't show param export menu", () => {
const wrapper = shallow(<HardwareSettings {...fakeProps()} />);
expect(wrapper.html()).not.toContain("fa-download");
});
}); });

View File

@ -0,0 +1,81 @@
jest.mock("../../actions", () => ({
toggleControlPanel: jest.fn(),
}));
import * as React from "react";
import { mount } from "enzyme";
import {
Highlight, HighlightProps, maybeHighlight, maybeOpenPanel, highlight
} from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { panelState } from "../../../__test_support__/control_panel_state";
import { toggleControlPanel } from "../../actions";
describe("<Highlight />", () => {
const fakeProps = (): HighlightProps => ({
settingName: DeviceSetting.motors,
children: <div />,
className: "section",
});
it("fades highlight", () => {
const p = fakeProps();
const wrapper = mount<Highlight>(<Highlight {...p} />);
wrapper.setState({ className: "highlight" });
wrapper.instance().componentDidMount();
expect(wrapper.state().className).toEqual("unhighlight");
});
});
describe("maybeHighlight()", () => {
beforeEach(() => {
highlight.opened = false;
highlight.highlighted = false;
});
it("highlights only once", () => {
location.search = "?highlight=motors";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("highlight");
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
it("doesn't highlight: different setting", () => {
location.search = "?highlight=name";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
it("doesn't highlight: no matches", () => {
location.search = "?highlight=na";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
});
describe("maybeOpenPanel()", () => {
beforeEach(() => {
highlight.opened = false;
highlight.highlighted = false;
});
it("opens panel only once", () => {
location.search = "?highlight=motors";
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).toHaveBeenCalledWith("motors");
jest.resetAllMocks();
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("doesn't open panel: already open", () => {
location.search = "?highlight=motors";
const panels = panelState();
panels.motors = true;
maybeOpenPanel(panels)(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("doesn't open panel: no search term", () => {
location.search = "";
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
});

View File

@ -4,12 +4,14 @@ import { settingToggle } from "../actions";
import { Row, Col, Help } from "../../ui/index"; import { Row, Col, Help } from "../../ui/index";
import { BooleanMCUInputGroupProps } from "./interfaces"; import { BooleanMCUInputGroupProps } from "./interfaces";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { t } from "../../i18next_wrapper";
import { Highlight } from "./maybe_highlight";
export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const { const {
tooltip, tooltip,
name, label,
x, x,
y, y,
z, z,
@ -26,40 +28,42 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const zParam = sourceFwConfig(z); const zParam = sourceFwConfig(z);
return <Row> return <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={label}>
<label> <Col xs={6} className={"widget-body-tooltips"}>
{name} <label>
{caution && {t(label)}
<i className="fa fa-exclamation-triangle caution-icon" />} {caution &&
</label> <i className="fa fa-exclamation-triangle caution-icon" />}
<Help text={tooltip} requireClick={true} position={Position.RIGHT} /> </label>
</Col> <Help text={tooltip} requireClick={true} position={Position.RIGHT} />
<Col xs={2} className={"centered-button-div"}> </Col>
<ToggleButton <Col xs={2} className={"centered-button-div"}>
grayscale={grayscale?.x} <ToggleButton
disabled={disable?.x} grayscale={grayscale?.x}
dim={!xParam.consistent} disabled={disable?.x}
toggleValue={xParam.value} dim={!xParam.consistent}
toggleAction={() => toggleValue={xParam.value}
dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> toggleAction={() =>
</Col> dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
<Col xs={2} className={"centered-button-div"}> </Col>
<ToggleButton <Col xs={2} className={"centered-button-div"}>
grayscale={grayscale?.y} <ToggleButton
disabled={disable?.y} grayscale={grayscale?.y}
dim={!yParam.consistent} disabled={disable?.y}
toggleValue={yParam.value} dim={!yParam.consistent}
toggleAction={() => toggleValue={yParam.value}
dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> toggleAction={() =>
</Col> dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
<Col xs={2} className={"centered-button-div"}> </Col>
<ToggleButton <Col xs={2} className={"centered-button-div"}>
grayscale={grayscale?.z} <ToggleButton
disabled={disable?.z} grayscale={grayscale?.z}
dim={!zParam.consistent} disabled={disable?.z}
toggleValue={zParam.value} dim={!zParam.consistent}
toggleAction={() => toggleValue={zParam.value}
dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> toggleAction={() =>
</Col> dispatch(settingToggle(z, sourceFwConfig, displayAlert))} />
</Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -4,8 +4,8 @@ import { t } from "../../i18next_wrapper";
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces"; import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui"; import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
import { save, edit } from "../../api/crud"; import { save, edit } from "../../api/crud";
import { MustBeOnline, isBotOnline } from "../must_be_online"; import { isBotOnline } from "../must_be_online";
import { Content } from "../../constants"; import { Content, DeviceSetting } from "../../constants";
import { TimezoneSelector } from "../timezones/timezone_selector"; import { TimezoneSelector } from "../timezones/timezone_selector";
import { timezoneMismatch } from "../timezones/guess_timezone"; import { timezoneMismatch } from "../timezones/guess_timezone";
import { CameraSelection } from "./fbos_settings/camera_selection"; import { CameraSelection } from "./fbos_settings/camera_selection";
@ -15,6 +15,8 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row";
import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector"; import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector";
import { ExternalUrl } from "../../external_urls";
import { Highlight } from "./maybe_highlight";
export enum ColWidth { export enum ColWidth {
label = 3, label = 3,
@ -22,15 +24,12 @@ export enum ColWidth {
button = 2 button = 2
} }
const OS_RELEASE_NOTES_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md";
export class FarmbotOsSettings export class FarmbotOsSettings
extends React.Component<FarmbotOsProps, FarmbotOsState> { extends React.Component<FarmbotOsProps, FarmbotOsState> {
state: FarmbotOsState = { allOsReleaseNotes: "" }; state: FarmbotOsState = { allOsReleaseNotes: "" };
componentDidMount() { componentDidMount() {
this.fetchReleaseNotes(OS_RELEASE_NOTES_URL); this.fetchReleaseNotes(ExternalUrl.osReleaseNotes);
} }
get osMajorVersion() { get osMajorVersion() {
@ -87,83 +86,79 @@ export class FarmbotOsSettings
</WidgetHeader> </WidgetHeader>
<WidgetBody> <WidgetBody>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.name}>
<label> <Col xs={ColWidth.label}>
{t("NAME")} <label>
</label> {t(DeviceSetting.name)}
</Col> </label>
<Col xs={9}> </Col>
<input name="name" <Col xs={9}>
onChange={this.changeBot} <input name="name"
onBlur={this.updateBot} onChange={this.changeBot}
value={this.props.deviceAccount.body.name} /> onBlur={this.updateBot}
</Col> value={this.props.deviceAccount.body.name} />
</Col>
</Highlight>
</Row> </Row>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.timezone}>
<label> <Col xs={ColWidth.label}>
{t("TIME ZONE")} <label>
</label> {t("TIME ZONE")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<div className="note"> <Col xs={ColWidth.description}>
{this.maybeWarnTz()} <div className="note">
</div> {this.maybeWarnTz()}
<div> </div>
<TimezoneSelector <div>
currentTimezone={this.props.deviceAccount.body.timezone} <TimezoneSelector
onUpdate={this.handleTimezone} /> currentTimezone={this.props.deviceAccount.body.timezone}
</div> onUpdate={this.handleTimezone} />
</Col> </div>
</Col>
</Highlight>
</Row> </Row>
<MustBeOnline <CameraSelection
syncStatus={sync_status} env={this.props.env}
networkState={this.props.botToMqttStatus} botOnline={botOnline}
lockOpen={process.env.NODE_ENV !== "production" saveFarmwareEnv={this.props.saveFarmwareEnv}
|| this.props.isValidFbosConfig}> shouldDisplay={this.props.shouldDisplay}
<CameraSelection dispatch={this.props.dispatch} />
env={this.props.env} <BoardType
botOnline={botOnline} botOnline={botOnline}
saveFarmwareEnv={this.props.saveFarmwareEnv} bot={bot}
shouldDisplay={this.props.shouldDisplay} alerts={this.props.alerts}
dispatch={this.props.dispatch} /> dispatch={this.props.dispatch}
<BoardType shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline} timeSettings={this.props.timeSettings}
bot={bot} sourceFbosConfig={sourceFbosConfig} />
alerts={this.props.alerts} <AutoUpdateRow
dispatch={this.props.dispatch} timeFormat={timeFormat}
shouldDisplay={this.props.shouldDisplay} device={this.props.deviceAccount}
timeSettings={this.props.timeSettings} dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} /> sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow <FarmbotOsRow
shouldDisplay={this.props.shouldDisplay} bot={this.props.bot}
timeFormat={timeFormat} osReleaseNotesHeading={this.osReleaseNotes.heading}
device={this.props.deviceAccount} osReleaseNotes={this.osReleaseNotes.notes}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} /> sourceFbosConfig={sourceFbosConfig}
<FarmbotOsRow shouldDisplay={this.props.shouldDisplay}
bot={this.props.bot} botOnline={botOnline}
osReleaseNotesHeading={this.osReleaseNotes.heading} botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()}
osReleaseNotes={this.osReleaseNotes.notes} timeSettings={this.props.timeSettings}
dispatch={this.props.dispatch} deviceAccount={this.props.deviceAccount} />
sourceFbosConfig={sourceFbosConfig} <AutoSyncRow
shouldDisplay={this.props.shouldDisplay} dispatch={this.props.dispatch}
botOnline={botOnline} sourceFbosConfig={sourceFbosConfig} />
botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()} {this.props.shouldDisplay(Feature.boot_sequence) &&
timeSettings={this.props.timeSettings} <BootSequenceSelector />}
deviceAccount={this.props.deviceAccount} /> <PowerAndReset
<AutoSyncRow controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} /> sourceFbosConfig={sourceFbosConfig}
{this.props.shouldDisplay(Feature.boot_sequence) && botOnline={botOnline} />
<BootSequenceSelector />}
<PowerAndReset
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline} />
</MustBeOnline>
</WidgetBody> </WidgetBody>
</form> </form>
</Widget>; </Widget>;

View File

@ -21,7 +21,6 @@ describe("<AutoUpdateRow/>", () => {
const fakeProps = (): AutoUpdateRowProps => ({ const fakeProps = (): AutoUpdateRowProps => ({
timeFormat: "12h", timeFormat: "12h",
shouldDisplay: jest.fn(() => true),
device: fakeDevice(), device: fakeDevice(),
dispatch: jest.fn(x => x(jest.fn(), () => state)), dispatch: jest.fn(x => x(jest.fn(), () => state)),
sourceFbosConfig: () => ({ value: 1, consistent: true }) sourceFbosConfig: () => ({ value: 1, consistent: true })

View File

@ -69,6 +69,9 @@ describe("<BoardType/>", () => {
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" }, { label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" }, { label: "Farmduino (Genesis v1.3)", value: "farmduino" },
{ label: "Farmduino (Genesis v1.4)", value: "farmduino_k14" }, { label: "Farmduino (Genesis v1.4)", value: "farmduino_k14" },
{ label: "Farmduino (Genesis v1.5)", value: "farmduino_k15" },
{ label: "Farmduino (Express v1.0)", value: "express_k10" },
{ label: "None", value: "none" },
]); ]);
}); });

View File

@ -22,7 +22,6 @@ describe("<FirmwareHardwareStatusDetails />", () => {
apiFirmwareValue: undefined, apiFirmwareValue: undefined,
botFirmwareValue: undefined, botFirmwareValue: undefined,
mcuFirmwareValue: undefined, mcuFirmwareValue: undefined,
shouldDisplay: () => true,
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
dispatch: jest.fn(), dispatch: jest.fn(),
}); });
@ -79,7 +78,6 @@ describe("<FirmwareHardwareStatus />", () => {
alerts: [], alerts: [],
botOnline: true, botOnline: true,
apiFirmwareValue: undefined, apiFirmwareValue: undefined,
shouldDisplay: () => true,
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
dispatch: jest.fn(), dispatch: jest.fn(),
}); });

View File

@ -25,30 +25,25 @@ describe("<PowerAndReset/>", () => {
const state = fakeState(); const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]); state.resources = buildResourceIndex([fakeConfig]);
const fakeProps = (): PowerAndResetProps => { const fakeProps = (): PowerAndResetProps => ({
return { controlPanelState: panelState(),
controlPanelState: panelState(), dispatch: jest.fn(x => x(jest.fn(), () => state)),
dispatch: jest.fn(x => x(jest.fn(), () => state)), sourceFbosConfig: () => ({ value: true, consistent: true }),
sourceFbosConfig: () => ({ value: true, consistent: true }), botOnline: true,
shouldDisplay: jest.fn(), });
botOnline: true,
};
};
it("open", () => { it("renders in open state", () => {
const p = fakeProps(); const p = fakeProps();
p.controlPanelState.power_and_reset = true; p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />); const wrapper = mount(<PowerAndReset {...p} />);
["Power and Reset", "Restart", "Shutdown", "Factory Reset", ["Power and Reset", "Restart", "Shutdown", "Restart Firmware",
"Automatic Factory Reset", "Connection Attempt Period", "Change Ownership"] "Factory Reset", "Automatic Factory Reset",
"Connection Attempt Period", "Change Ownership"]
.map(string => expect(wrapper.text().toLowerCase()) .map(string => expect(wrapper.text().toLowerCase())
.toContain(string.toLowerCase())); .toContain(string.toLowerCase()));
["Restart Firmware"]
.map(string => expect(wrapper.text().toLowerCase())
.not.toContain(string.toLowerCase()));
}); });
it("closed", () => { it("renders as closed", () => {
const p = fakeProps(); const p = fakeProps();
p.controlPanelState.power_and_reset = false; p.controlPanelState.power_and_reset = false;
const wrapper = mount(<PowerAndReset {...p} />); const wrapper = mount(<PowerAndReset {...p} />);
@ -73,7 +68,7 @@ describe("<PowerAndReset/>", () => {
p.sourceFbosConfig = () => ({ value: false, consistent: true }); p.sourceFbosConfig = () => ({ value: false, consistent: true });
p.controlPanelState.power_and_reset = true; p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />); const wrapper = mount(<PowerAndReset {...p} />);
clickButton(wrapper, 3, "yes"); clickButton(wrapper, 4, "yes");
expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true }); expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid); expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
}); });
@ -81,7 +76,6 @@ describe("<PowerAndReset/>", () => {
it("restarts firmware", () => { it("restarts firmware", () => {
const p = fakeProps(); const p = fakeProps();
p.controlPanelState.power_and_reset = true; p.controlPanelState.power_and_reset = true;
p.shouldDisplay = () => true;
const wrapper = mount(<PowerAndReset {...p} />); const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase()) expect(wrapper.text().toLowerCase())
.toContain("Restart Firmware".toLowerCase()); .toContain("Restart Firmware".toLowerCase());

View File

@ -1,32 +1,35 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { updateConfig } from "../../actions"; import { updateConfig } from "../../actions";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { AutoSyncRowProps } from "./interfaces"; import { AutoSyncRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function AutoSyncRow(props: AutoSyncRowProps) { export function AutoSyncRow(props: AutoSyncRowProps) {
const autoSync = props.sourceFbosConfig("auto_sync"); const autoSync = props.sourceFbosConfig("auto_sync");
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.autoSync}>
<label> <Col xs={ColWidth.label}>
{t("AUTO SYNC")} <label>
</label> {t("AUTO SYNC")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.AUTO_SYNC)} <p>
</p> {t(Content.AUTO_SYNC)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<ToggleButton <Col xs={ColWidth.button}>
toggleValue={autoSync.value} <ToggleButton
dim={!autoSync.consistent} toggleValue={autoSync.value}
toggleAction={() => { dim={!autoSync.consistent}
props.dispatch(updateConfig({ auto_sync: !autoSync.value })); toggleAction={() => {
}} /> props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
</Col> }} />
</Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -3,39 +3,41 @@ import { Row, Col } from "../../../ui/index";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { updateConfig } from "../../actions"; import { updateConfig } from "../../actions";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { AutoUpdateRowProps } from "./interfaces"; import { AutoUpdateRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector"; import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector";
import { Feature } from "../../interfaces"; import { Highlight } from "../maybe_highlight";
export function AutoUpdateRow(props: AutoUpdateRowProps) { export function AutoUpdateRow(props: AutoUpdateRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
return <div> return <div>
{props.shouldDisplay(Feature.ota_update_hour) && <OtaTimeSelector <OtaTimeSelector
timeFormat={props.timeFormat} timeFormat={props.timeFormat}
disabled={!osAutoUpdate.value} disabled={!osAutoUpdate.value}
value={props.device.body.ota_hour} value={props.device.body.ota_hour}
onChange={changeOtaHour(props.dispatch, props.device)} />} onChange={changeOtaHour(props.dispatch, props.device)} />
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<label> <Col xs={ColWidth.label}>
{t("FARMBOT OS AUTO UPDATE")} <label>
</label> {t(DeviceSetting.farmbotOSAutoUpdate)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.OS_AUTO_UPDATE)} <p>
</p> {t(Content.OS_AUTO_UPDATE)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<ToggleButton toggleValue={osAutoUpdate.value} <Col xs={ColWidth.button}>
dim={!osAutoUpdate.consistent} <ToggleButton toggleValue={osAutoUpdate.value}
toggleAction={() => props.dispatch(updateConfig({ dim={!osAutoUpdate.consistent}
os_auto_update: !osAutoUpdate.value toggleAction={() => props.dispatch(updateConfig({
}))} /> os_auto_update: !osAutoUpdate.value
</Col> }))} />
</Col>
</Highlight>
</Row> </Row>
</div>; </div>;
} }

View File

@ -10,6 +10,8 @@ import { FirmwareHardwareStatus } from "./firmware_hardware_status";
import { import {
isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI
} from "../firmware_hardware_support"; } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
interface BoardTypeState { sending: boolean } interface BoardTypeState { sending: boolean }
@ -47,31 +49,32 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
render() { render() {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.firmware}>
<label> <Col xs={ColWidth.label}>
{t("FIRMWARE")} <label>
</label> {t("FIRMWARE")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<div> <Col xs={ColWidth.description}>
<FBSelect <div>
key={this.apiValue} <FBSelect
extraClass={this.state.sending ? "dim" : ""} key={this.apiValue}
list={getFirmwareChoices(this.props.shouldDisplay)} extraClass={this.state.sending ? "dim" : ""}
selectedItem={this.selectedBoard} list={getFirmwareChoices()}
onChange={this.sendOffConfig} /> selectedItem={this.selectedBoard}
</div> onChange={this.sendOffConfig} />
</Col> </div>
<Col xs={ColWidth.button}> </Col>
<FirmwareHardwareStatus <Col xs={ColWidth.button}>
botOnline={this.props.botOnline} <FirmwareHardwareStatus
apiFirmwareValue={this.apiValue} botOnline={this.props.botOnline}
alerts={this.props.alerts} apiFirmwareValue={this.apiValue}
bot={this.props.bot} alerts={this.props.alerts}
dispatch={this.props.dispatch} bot={this.props.bot}
timeSettings={this.props.timeSettings} dispatch={this.props.dispatch}
shouldDisplay={this.props.shouldDisplay} /> timeSettings={this.props.timeSettings} />
</Col> </Col>
</Highlight>
</Row>; </Row>;
} }
} }

View File

@ -9,6 +9,8 @@ import { selectAllSequences, findSequenceById } from "../../../resources/selecto
import { betterCompact } from "../../../util"; import { betterCompact } from "../../../util";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
interface Props { interface Props {
list: DropDownItem[]; list: DropDownItem[];
@ -56,18 +58,20 @@ export class RawBootSequenceSelector extends React.Component<Props, {}> {
render() { render() {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.bootSequence}>
<label> <Col xs={ColWidth.label}>
{t("BOOT SEQUENCE")} <label>
</label> {t("BOOT SEQUENCE")}
</Col> </label>
<Col xs={7}> </Col>
<FBSelect <Col xs={7}>
allowEmpty={true} <FBSelect
list={this.props.list} allowEmpty={true}
selectedItem={this.props.selectedItem} list={this.props.list}
onChange={this.onChange} /> selectedItem={this.props.selectedItem}
</Col> onChange={this.onChange} />
</Col>
</Highlight>
</Row>; </Row>;
} }
} }

View File

@ -8,7 +8,8 @@ import { getDevice } from "../../../device";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { Feature, UserEnv } from "../../interfaces"; import { Feature, UserEnv } from "../../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Content, ToolTips } from "../../../constants"; import { Content, ToolTips, DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
/** Check if the camera has been disabled. */ /** Check if the camera has been disabled. */
export const cameraDisabled = (env: UserEnv): boolean => export const cameraDisabled = (env: UserEnv): boolean =>
@ -84,21 +85,23 @@ export class CameraSelection
render() { render() {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.camera}>
<label> <Col xs={ColWidth.label}>
{t("CAMERA")} <label>
</label> {t("CAMERA")}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<div> <Col xs={ColWidth.description}>
<FBSelect <div>
allowEmpty={false} <FBSelect
list={CAMERA_CHOICES()} allowEmpty={false}
selectedItem={this.selectedCamera()} list={CAMERA_CHOICES()}
onChange={this.sendOffConfig} selectedItem={this.selectedCamera()}
extraClass={this.props.botOnline ? "" : "disabled"} /> onChange={this.sendOffConfig}
</div> extraClass={this.props.botOnline ? "" : "disabled"} />
</Col> </div>
</Col>
</Highlight>
</Row>; </Row>;
} }
} }

View File

@ -1,12 +1,13 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { factoryReset, updateConfig } from "../../actions"; import { factoryReset, updateConfig } from "../../actions";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { BotConfigInputBox } from "../bot_config_input_box"; import { BotConfigInputBox } from "../bot_config_input_box";
import { FactoryResetRowProps } from "./interfaces"; import { FactoryResetRowProps } from "./interfaces";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function FactoryResetRow(props: FactoryResetRowProps) { export function FactoryResetRow(props: FactoryResetRowProps) {
const { dispatch, sourceFbosConfig, botOnline } = props; const { dispatch, sourceFbosConfig, botOnline } = props;
@ -14,66 +15,72 @@ export function FactoryResetRow(props: FactoryResetRowProps) {
const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {}; const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {};
return <div> return <div>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.factoryReset}>
<label> <Col xs={ColWidth.label}>
{t("Factory Reset")} <label>
</label> {t(DeviceSetting.factoryReset)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.FACTORY_RESET_WARNING)} <p>
</p> {t(Content.FACTORY_RESET_WARNING)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<button <Col xs={ColWidth.button}>
className="fb-button red" <button
type="button" className="fb-button red"
onClick={factoryReset} type="button"
disabled={!botOnline}> onClick={factoryReset}
{t("FACTORY RESET")} disabled={!botOnline}>
</button> {t("FACTORY RESET")}
</Col> </button>
</Col>
</Highlight>
</Row> </Row>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.autoFactoryReset}>
<label> <Col xs={ColWidth.label}>
{t("Automatic Factory Reset")} <label>
</label> {t(DeviceSetting.autoFactoryReset)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(Content.AUTO_FACTORY_RESET)} <p>
</p> {t(Content.AUTO_FACTORY_RESET)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<ToggleButton <Col xs={ColWidth.button}>
toggleValue={!disableFactoryReset.value} <ToggleButton
dim={!disableFactoryReset.consistent} toggleValue={!disableFactoryReset.value}
toggleAction={() => { dim={!disableFactoryReset.consistent}
dispatch(updateConfig({ toggleAction={() => {
disable_factory_reset: !disableFactoryReset.value dispatch(updateConfig({
})); disable_factory_reset: !disableFactoryReset.value
}} /> }));
</Col> }} />
</Col>
</Highlight>
</Row> </Row>
<Row> <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.connectionAttemptPeriod}>
<label style={maybeDisableTimer}> <Col xs={ColWidth.label}>
{t("Connection Attempt Period")} <label style={maybeDisableTimer}>
</label> {t(DeviceSetting.connectionAttemptPeriod)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p style={maybeDisableTimer}> <Col xs={ColWidth.description}>
{t(Content.AUTO_FACTORY_RESET_PERIOD)} <p style={maybeDisableTimer}>
</p> {t(Content.AUTO_FACTORY_RESET_PERIOD)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<BotConfigInputBox <Col xs={ColWidth.button}>
setting="network_not_found_timer" <BotConfigInputBox
dispatch={dispatch} setting="network_not_found_timer"
disabled={!!disableFactoryReset.value} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} /> disabled={!!disableFactoryReset.value}
</Col> sourceFbosConfig={sourceFbosConfig} />
</Col>
</Highlight>
</Row> </Row>
</div>; </div>;
} }

View File

@ -7,6 +7,8 @@ import { FarmbotOsRowProps } from "./interfaces";
import { FbosDetails } from "./fbos_details"; import { FbosDetails } from "./fbos_details";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { ErrorBoundary } from "../../../error_boundary"; import { ErrorBoundary } from "../../../error_boundary";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
const getVersionString = const getVersionString =
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => { (fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
@ -21,48 +23,50 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
} = bot.hardware.informational_settings; } = bot.hardware.informational_settings;
const version = getVersionString(controller_version, currently_on_beta); const version = getVersionString(controller_version, currently_on_beta);
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={DeviceSetting.farmbotOS}>
<label> <Col xs={ColWidth.label}>
{t("FARMBOT OS")} <label>
</label> {t(DeviceSetting.farmbotOS)}
</Col> </label>
<Col xs={3}> </Col>
<Popover position={Position.BOTTOM_LEFT}> <Col xs={3}>
<p> <Popover position={Position.BOTTOM_LEFT}>
{t("Version {{ version }}", { version })} <p>
</p> {t("Version {{ version }}", { version })}
<ErrorBoundary> </p>
<FbosDetails <ErrorBoundary>
botInfoSettings={bot.hardware.informational_settings} <FbosDetails
dispatch={dispatch} botInfoSettings={bot.hardware.informational_settings}
shouldDisplay={props.shouldDisplay} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} shouldDisplay={props.shouldDisplay}
botToMqttLastSeen={props.botToMqttLastSeen} sourceFbosConfig={sourceFbosConfig}
timeSettings={props.timeSettings} botToMqttLastSeen={props.botToMqttLastSeen}
deviceAccount={props.deviceAccount} /> timeSettings={props.timeSettings}
</ErrorBoundary> deviceAccount={props.deviceAccount} />
</Popover> </ErrorBoundary>
</Col> </Popover>
<Col xs={3}> </Col>
<Popover position={Position.BOTTOM}> <Col xs={3}>
<p className="release-notes-button"> <Popover position={Position.BOTTOM}>
{t("Release Notes")}&nbsp; <p className="release-notes-button">
{t("Release Notes")}&nbsp;
<i className="fa fa-caret-down" /> <i className="fa fa-caret-down" />
</p> </p>
<div className="release-notes"> <div className="release-notes">
<h1>{props.osReleaseNotesHeading}</h1> <h1>{props.osReleaseNotesHeading}</h1>
<Markdown> <Markdown>
{osReleaseNotes} {osReleaseNotes}
</Markdown> </Markdown>
</div> </div>
</Popover> </Popover>
</Col> </Col>
<Col xs={3}> <Col xs={3}>
<OsUpdateButton <OsUpdateButton
bot={bot} bot={bot}
sourceFbosConfig={sourceFbosConfig} sourceFbosConfig={sourceFbosConfig}
shouldDisplay={props.shouldDisplay} shouldDisplay={props.shouldDisplay}
botOnline={botOnline} /> botOnline={botOnline} />
</Col> </Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -2,10 +2,12 @@ import * as React from "react";
import { Row, Col } from "../../../ui"; import { Row, Col } from "../../../ui";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
export interface FbosButtonRowProps { export interface FbosButtonRowProps {
botOnline: boolean; botOnline: boolean;
label: string; label: DeviceSetting;
description: string; description: string;
buttonText: string; buttonText: string;
color: string; color: string;
@ -14,24 +16,26 @@ export interface FbosButtonRowProps {
export const FbosButtonRow = (props: FbosButtonRowProps) => { export const FbosButtonRow = (props: FbosButtonRowProps) => {
return <Row> return <Row>
<Col xs={ColWidth.label}> <Highlight settingName={props.label}>
<label> <Col xs={ColWidth.label}>
{t(props.label)} <label>
</label> {t(props.label)}
</Col> </label>
<Col xs={ColWidth.description}> </Col>
<p> <Col xs={ColWidth.description}>
{t(props.description)} <p>
</p> {t(props.description)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>
<button <Col xs={ColWidth.button}>
className={`fb-button ${props.color}`} <button
type="button" className={`fb-button ${props.color}`}
onClick={props.action} type="button"
disabled={!props.botOnline}> onClick={props.action}
{t(props.buttonText)} disabled={!props.botOnline}>
</button> {t(props.buttonText)}
</Col> </button>
</Col>
</Highlight>
</Row>; </Row>;
}; };

View File

@ -14,6 +14,7 @@ import { timeFormatString } from "../../../util";
import { TimeSettings } from "../../../interfaces"; import { TimeSettings } from "../../../interfaces";
import { StringConfigKey } from "farmbot/dist/resources/configs/fbos"; import { StringConfigKey } from "farmbot/dist/resources/configs/fbos";
import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
import { ExternalUrl, FarmBotRepo } from "../../../external_urls";
/** Return an indicator color for the given temperature (C). */ /** Return an indicator color for the given temperature (C). */
export const colorFromTemp = (temp: number | undefined): string => { export const colorFromTemp = (temp: number | undefined): string => {
@ -170,7 +171,7 @@ const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8);
interface CommitDisplayProps { interface CommitDisplayProps {
title: string; title: string;
repo: string; repo: FarmBotRepo;
commit: string; commit: string;
} }
@ -184,7 +185,7 @@ const CommitDisplay = (
{shortCommit === "---" {shortCommit === "---"
? shortCommit ? shortCommit
: <a : <a
href={`https://github.com/FarmBot/${repo}/tree/${shortCommit}`} href={`${ExternalUrl.gitHubFarmBot}/${repo}/tree/${shortCommit}`}
target="_blank"> target="_blank">
{shortCommit} {shortCommit}
</a>} </a>}
@ -270,14 +271,15 @@ export function FbosDetails(props: FbosDetailsProps) {
timeSettings={props.timeSettings} timeSettings={props.timeSettings}
device={props.deviceAccount} /> device={props.deviceAccount} />
<p><b>{t("Environment")}: </b>{env}</p> <p><b>{t("Environment")}: </b>{env}</p>
<CommitDisplay title={t("Commit")} repo={"farmbot_os"} commit={commit} /> <CommitDisplay title={t("Commit")}
repo={FarmBotRepo.FarmBotOS} commit={commit} />
<p><b>{t("Target")}: </b>{target}</p> <p><b>{t("Target")}: </b>{target}</p>
<p><b>{t("Node name")}: </b>{last((node_name || "").split("@"))}</p> <p><b>{t("Node name")}: </b>{last((node_name || "").split("@"))}</p>
<p><b>{t("Device ID")}: </b>{props.deviceAccount.body.id}</p> <p><b>{t("Device ID")}: </b>{props.deviceAccount.body.id}</p>
{isString(private_ip) && <p><b>{t("Local IP address")}: </b>{private_ip}</p>} {isString(private_ip) && <p><b>{t("Local IP address")}: </b>{private_ip}</p>}
<p><b>{t("Firmware")}: </b>{reformatFwVersion(firmware_version)}</p> <p><b>{t("Firmware")}: </b>{reformatFwVersion(firmware_version)}</p>
<CommitDisplay title={t("Firmware commit")} <CommitDisplay title={t("Firmware commit")}
repo={"farmbot-arduino-firmware"} commit={firmwareCommit} /> repo={FarmBotRepo.FarmBotArduinoFirmware} commit={firmwareCommit} />
<p><b>{t("Firmware code")}: </b>{firmware_version}</p> <p><b>{t("Firmware code")}: </b>{firmware_version}</p>
{isNumber(uptime) && <UptimeDisplay uptime_sec={uptime} />} {isNumber(uptime) && <UptimeDisplay uptime_sec={uptime} />}
{isNumber(memory_usage) && {isNumber(memory_usage) &&

View File

@ -3,7 +3,7 @@ import { Popover, Position } from "@blueprintjs/core";
import { FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; import { FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
import { flashFirmware } from "../../actions"; import { flashFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { BotState, Feature, ShouldDisplay } from "../../interfaces"; import { BotState } from "../../interfaces";
import { FirmwareAlerts } from "../../../messages/alerts"; import { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces"; import { TimeSettings } from "../../../interfaces";
import { trim } from "../../../util"; import { trim } from "../../../util";
@ -36,7 +36,6 @@ export interface FirmwareHardwareStatusDetailsProps {
apiFirmwareValue: string | undefined; apiFirmwareValue: string | undefined;
botFirmwareValue: string | undefined; botFirmwareValue: string | undefined;
mcuFirmwareValue: string | undefined; mcuFirmwareValue: string | undefined;
shouldDisplay: ShouldDisplay;
timeSettings: TimeSettings; timeSettings: TimeSettings;
dispatch: Function; dispatch: Function;
} }
@ -81,13 +80,10 @@ export const FirmwareHardwareStatusDetails =
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p> <p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
<label>{t("Arduino/Farmduino")}</label> <label>{t("Arduino/Farmduino")}</label>
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p> <p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
{props.shouldDisplay(Feature.flash_firmware) && <label>{t("Actions")}</label>
<div> <FirmwareActions
<label>{t("Actions")}</label> apiFirmwareValue={props.apiFirmwareValue}
<FirmwareActions botOnline={props.botOnline} />
apiFirmwareValue={props.apiFirmwareValue}
botOnline={props.botOnline} />
</div>}
<FirmwareAlerts <FirmwareAlerts
alerts={props.alerts} alerts={props.alerts}
dispatch={props.dispatch} dispatch={props.dispatch}
@ -102,7 +98,6 @@ export interface FirmwareHardwareStatusProps {
bot: BotState; bot: BotState;
botOnline: boolean; botOnline: boolean;
timeSettings: TimeSettings; timeSettings: TimeSettings;
shouldDisplay: ShouldDisplay;
dispatch: Function; dispatch: Function;
} }
@ -122,7 +117,6 @@ export const FirmwareHardwareStatus = (props: FirmwareHardwareStatusProps) => {
botFirmwareValue={firmware_hardware} botFirmwareValue={firmware_hardware}
mcuFirmwareValue={boardType(firmware_version)} mcuFirmwareValue={boardType(firmware_version)}
timeSettings={props.timeSettings} timeSettings={props.timeSettings}
dispatch={props.dispatch} dispatch={props.dispatch} />
shouldDisplay={props.shouldDisplay} />
</Popover>; </Popover>;
}; };

View File

@ -24,7 +24,6 @@ export interface AutoUpdateRowProps {
timeFormat: PreferredHourFormat; timeFormat: PreferredHourFormat;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;
device: TaggedDevice; device: TaggedDevice;
shouldDisplay: ShouldDisplay;
} }
export interface CameraSelectionProps { export interface CameraSelectionProps {
@ -53,7 +52,6 @@ export interface PowerAndResetProps {
controlPanelState: ControlPanelState; controlPanelState: ControlPanelState;
dispatch: Function; dispatch: Function;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
botOnline: boolean; botOnline: boolean;
} }

View File

@ -4,59 +4,58 @@ import { Collapse, Popover, Position } from "@blueprintjs/core";
import { FactoryResetRow } from "./factory_reset_row"; import { FactoryResetRow } from "./factory_reset_row";
import { PowerAndResetProps } from "./interfaces"; import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form"; import { ChangeOwnershipForm } from "./change_ownership_form";
import { Feature } from "../../interfaces";
import { FbosButtonRow } from "./fbos_button_row"; import { FbosButtonRow } from "./fbos_button_row";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions"; import { reboot, powerOff, restartFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function PowerAndReset(props: PowerAndResetProps) { export function PowerAndReset(props: PowerAndResetProps) {
const { dispatch, sourceFbosConfig, shouldDisplay, botOnline } = props; const { dispatch, sourceFbosConfig, botOnline } = props;
const { power_and_reset } = props.controlPanelState; const { power_and_reset } = props.controlPanelState;
return <section> return <Highlight className={"section"}
<div style={{ fontSize: "1px" }}> settingName={DeviceSetting.powerAndReset}>
<Header <Header
expanded={power_and_reset} expanded={power_and_reset}
title={t("Power and Reset")} title={DeviceSetting.powerAndReset}
name={"power_and_reset"} panel={"power_and_reset"}
dispatch={dispatch} /> dispatch={dispatch} />
</div>
<Collapse isOpen={!!power_and_reset}> <Collapse isOpen={!!power_and_reset}>
<FbosButtonRow <FbosButtonRow
botOnline={botOnline} botOnline={botOnline}
label={t("RESTART FARMBOT")} label={DeviceSetting.restartFarmbot}
description={Content.RESTART_FARMBOT} description={Content.RESTART_FARMBOT}
buttonText={t("RESTART")} buttonText={t("RESTART")}
color={"yellow"} color={"yellow"}
action={reboot} /> action={reboot} />
<FbosButtonRow <FbosButtonRow
botOnline={botOnline} botOnline={botOnline}
label={t("SHUTDOWN FARMBOT")} label={DeviceSetting.shutdownFarmbot}
description={Content.SHUTDOWN_FARMBOT} description={Content.SHUTDOWN_FARMBOT}
buttonText={t("SHUTDOWN")} buttonText={t("SHUTDOWN")}
color={"red"} color={"red"}
action={powerOff} /> action={powerOff} />
{shouldDisplay(Feature.firmware_restart) && <FbosButtonRow
<FbosButtonRow botOnline={botOnline}
botOnline={botOnline} label={DeviceSetting.restartFirmware}
label={t("RESTART FIRMWARE")} description={Content.RESTART_FIRMWARE}
description={Content.RESTART_FIRMWARE} buttonText={t("RESTART")}
buttonText={t("RESTART")} color={"yellow"}
color={"yellow"} action={restartFirmware} />
action={restartFirmware} />}
<FactoryResetRow <FactoryResetRow
dispatch={dispatch} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} /> botOnline={botOnline} />
{botOnline && {botOnline &&
<Popover position={Position.BOTTOM_LEFT}> <Highlight settingName={DeviceSetting.changeOwnership}>
<p className={"release-notes-button"}> <Popover position={Position.BOTTOM_LEFT}>
{t("Change Ownership")}&nbsp; <p className={"release-notes-button"}>
<i className="fa fa-caret-down" /> {t(DeviceSetting.changeOwnership)}&nbsp;
</p> <i className="fa fa-caret-down" />
<ChangeOwnershipForm /> </p>
</Popover> <ChangeOwnershipForm />
} </Popover>
</Highlight>}
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,5 +1,4 @@
import { FirmwareHardware } from "farmbot"; import { FirmwareHardware, TaggedFbosConfig } from "farmbot";
import { ShouldDisplay, Feature } from "../interfaces";
export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
const values: FirmwareHardware[] = [ const values: FirmwareHardware[] = [
@ -11,6 +10,12 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
return !!values.includes(x as FirmwareHardware); return !!values.includes(x as FirmwareHardware);
}; };
export const getFwHardwareValue =
(fbosConfig: TaggedFbosConfig | undefined) => {
const value = fbosConfig?.body.firmware_hardware;
return isFwHardwareValue(value) ? value : undefined;
};
const TMC_BOARDS = ["express_k10", "farmduino_k15"]; const TMC_BOARDS = ["express_k10", "farmduino_k15"];
const EXPRESS_BOARDS = ["express_k10"]; const EXPRESS_BOARDS = ["express_k10"];
@ -77,12 +82,11 @@ export const FIRMWARE_CHOICES_DDI = {
[NONE.value]: NONE [NONE.value]: NONE
}; };
export const getFirmwareChoices = export const getFirmwareChoices = () => ([
(shouldDisplay: ShouldDisplay = () => true) => ([ ARDUINO,
ARDUINO, FARMDUINO,
FARMDUINO, FARMDUINO_K14,
FARMDUINO_K14, FARMDUINO_K15,
...(shouldDisplay(Feature.farmduino_k15) ? [FARMDUINO_K15] : []), EXPRESS_K10,
...(shouldDisplay(Feature.express_k10) ? [EXPRESS_K10] : []), NONE,
...(shouldDisplay(Feature.none_firmware) ? [NONE] : []), ]);
]);

View File

@ -2,11 +2,12 @@ import * as React from "react";
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions"; import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index"; import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
import { HardwareSettingsProps } from "../interfaces"; import { HardwareSettingsProps } from "../interfaces";
import { MustBeOnline, isBotOnline } from "../must_be_online"; import { isBotOnline } from "../must_be_online";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone"; import { DangerZone } from "./hardware_settings/danger_zone";
import { PinGuard } from "./hardware_settings/pin_guard"; import { PinGuard } from "./hardware_settings/pin_guard";
import { EncodersAndEndStops } from "./hardware_settings/encoders_and_endstops"; import { Encoders } from "./hardware_settings/encoders";
import { EndStops } from "./hardware_settings/endstops";
import { Motors } from "./hardware_settings/motors"; import { Motors } from "./hardware_settings/motors";
import { SpacePanelHeader } from "./hardware_settings/space_panel_header"; import { SpacePanelHeader } from "./hardware_settings/space_panel_header";
import { import {
@ -15,10 +16,16 @@ import {
import { Popover, Position } from "@blueprintjs/core"; import { Popover, Position } from "@blueprintjs/core";
import { FwParamExportMenu } from "./hardware_settings/export_menu"; import { FwParamExportMenu } from "./hardware_settings/export_menu";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling";
import { maybeOpenPanel } from "./maybe_highlight";
export class HardwareSettings extends export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> { React.Component<HardwareSettingsProps, {}> {
componentDidMount = () =>
this.props.dispatch(maybeOpenPanel(this.props.controlPanelState));
render() { render() {
const { const {
bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig, bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig,
@ -27,15 +34,9 @@ export class HardwareSettings extends
const { informational_settings } = this.props.bot.hardware; const { informational_settings } = this.props.bot.hardware;
const { sync_status } = informational_settings; const { sync_status } = informational_settings;
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus); const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget"> return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}> <WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
<MustBeOnline
hideBanner={true}
syncStatus={sync_status}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
</MustBeOnline>
</WidgetHeader>
<WidgetBody> <WidgetBody>
<button <button
className={"fb-button gray no-float"} className={"fb-button gray no-float"}
@ -47,46 +48,37 @@ export class HardwareSettings extends
onClick={() => dispatch(bulkToggleControlPanel(false))}> onClick={() => dispatch(bulkToggleControlPanel(false))}>
{t("Collapse All")} {t("Collapse All")}
</button> </button>
{firmwareConfig && <Popover position={Position.BOTTOM_RIGHT}>
<Popover position={Position.BOTTOM_RIGHT}> <i className="fa fa-download" />
<i className="fa fa-download" /> <FwParamExportMenu firmwareConfig={firmwareConfig} />
<FwParamExportMenu firmwareConfig={firmwareConfig} /> </Popover>
</Popover>} <div className="label-headings">
<MustBeOnline <SpacePanelHeader />
networkState={this.props.botToMqttStatus} </div>
syncStatus={sync_status} <HomingAndCalibration {...commonProps}
lockOpen={process.env.NODE_ENV !== "production" || !!firmwareConfig}> bot={bot}
<div className="label-headings"> sourceFwConfig={sourceFwConfig}
<SpacePanelHeader /> firmwareConfig={firmwareConfig}
</div> firmwareHardware={firmwareHardware}
<HomingAndCalibration botDisconnected={botDisconnected} />
dispatch={dispatch} <Motors {...commonProps}
bot={bot} sourceFwConfig={sourceFwConfig}
sourceFwConfig={sourceFwConfig} firmwareHardware={firmwareHardware} />
firmwareConfig={firmwareConfig} <Encoders {...commonProps}
botDisconnected={botDisconnected} /> sourceFwConfig={sourceFwConfig}
<Motors firmwareHardware={firmwareHardware} />
dispatch={dispatch} <EndStops {...commonProps}
controlPanelState={controlPanelState} sourceFwConfig={sourceFwConfig} />
sourceFwConfig={sourceFwConfig} <ErrorHandling {...commonProps}
firmwareHardware={firmwareHardware} /> sourceFwConfig={sourceFwConfig} />
<EncodersAndEndStops <PinGuard {...commonProps}
dispatch={dispatch} resources={resources}
shouldDisplay={this.props.shouldDisplay} sourceFwConfig={sourceFwConfig} />
controlPanelState={controlPanelState} <DangerZone {...commonProps}
sourceFwConfig={sourceFwConfig} onReset={MCUFactoryReset}
firmwareHardware={firmwareHardware} /> botDisconnected={botDisconnected} />
<PinGuard <PinBindings {...commonProps}
dispatch={dispatch} resources={resources} />
resources={resources}
controlPanelState={controlPanelState}
sourceFwConfig={sourceFwConfig} />
<DangerZone
dispatch={dispatch}
controlPanelState={controlPanelState}
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
</MustBeOnline>
</WidgetBody> </WidgetBody>
</Widget>; </Widget>;
} }

View File

@ -1,22 +1,41 @@
const mockDevice = {
calibrate: jest.fn(() => Promise.resolve({}))
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react"; import * as React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { CalibrationRow } from "../calibration_row"; import { CalibrationRow } from "../calibration_row";
import { bot } from "../../../../__test_support__/fake_state/bot"; import { bot } from "../../../../__test_support__/fake_state/bot";
import { CalibrationRowProps } from "../../interfaces";
import { DeviceSetting } from "../../../../constants";
describe("<CalibrationRow />", () => {
const fakeProps = (): CalibrationRowProps => ({
type: "calibrate",
hardware: bot.hardware.mcu_params,
botDisconnected: false,
action: jest.fn(),
toolTip: "calibrate",
title: DeviceSetting.calibration,
axisTitle: "calibrate",
});
describe("<HomingRow />", () => {
it("calls device", () => { it("calls device", () => {
const result = mount(<CalibrationRow const p = fakeProps();
hardware={bot.hardware.mcu_params} const result = mount(<CalibrationRow {...p} />);
botDisconnected={false} />); p.hardware.encoder_enabled_x = 1;
p.hardware.encoder_enabled_y = 1;
p.hardware.encoder_enabled_z = 0;
[0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click")); [0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click"));
expect(mockDevice.calibrate).toHaveBeenCalledTimes(2); expect(p.action).toHaveBeenCalledTimes(2);
[{ axis: "y" }, { axis: "x" }].map(x => ["y", "x"].map(x => expect(p.action).toHaveBeenCalledWith(x));
expect(mockDevice.calibrate).toHaveBeenCalledWith(x)); });
it("is not disabled", () => {
const p = fakeProps();
p.type = "zero";
const result = mount(<CalibrationRow {...p} />);
p.hardware.encoder_enabled_x = 0;
p.hardware.encoder_enabled_y = 1;
p.hardware.encoder_enabled_z = 0;
[0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click"));
expect(p.action).toHaveBeenCalledTimes(3);
["x", "y", "z"].map(x => expect(p.action).toHaveBeenCalledWith(x));
}); });
}); });

View File

@ -1,47 +0,0 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { EncodersAndEndStops } from "../encoders_and_endstops";
import { EncodersProps, NumericMCUInputGroupProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { Dictionary } from "farmbot";
describe("<EncodersAndEndStops />", () => {
const mockFeatures: Dictionary<boolean> = {};
const fakeProps = (): EncodersProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
shouldDisplay: jest.fn(key => mockFeatures[key]),
firmwareHardware: undefined,
});
it("shows encoder labels", () => {
const p = fakeProps();
p.firmwareHardware = undefined;
const wrapper = mount(<EncodersAndEndStops {...p} />);
expect(wrapper.text().toLowerCase()).toContain("encoder");
expect(wrapper.text().toLowerCase()).not.toContain("stall");
});
it("shows stall labels", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<EncodersAndEndStops {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
expect(wrapper.text().toLowerCase()).toContain("stall");
});
it.each<["short" | "long"]>([
["short"],
["long"],
])("uses %s int scaling factor", (size) => {
mockFeatures.long_scaling_factor = size === "short" ? false : true;
const wrapper = shallow(<EncodersAndEndStops {...fakeProps()} />);
const sfProps = wrapper.find("NumericMCUInputGroup").at(2)
.props() as NumericMCUInputGroupProps;
expect(sfProps.name).toEqual("Encoder Scaling");
expect(sfProps.intSize).toEqual(size);
});
});

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { mount } from "enzyme";
import { Encoders } from "../encoders";
import { EncodersProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<Encoders />", () => {
const fakeProps = (): EncodersProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
firmwareHardware: undefined,
});
it("shows encoder labels", () => {
const p = fakeProps();
p.firmwareHardware = undefined;
const wrapper = mount(<Encoders {...p} />);
expect(wrapper.text().toLowerCase()).toContain("encoder");
expect(wrapper.text().toLowerCase()).not.toContain("stall");
});
it("shows stall labels", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Encoders {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
expect(wrapper.text().toLowerCase()).toContain("stall");
});
});

View File

@ -0,0 +1,21 @@
import * as React from "react";
import { mount } from "enzyme";
import { EndStops } from "../endstops";
import { EndStopsProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<EndStops />", () => {
const fakeProps = (): EndStopsProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
});
it("shows endstop labels", () => {
const p = fakeProps();
const wrapper = mount(<EndStops {...p} />);
expect(wrapper.text().toLowerCase()).toContain("endstop");
});
});

View File

@ -0,0 +1,47 @@
jest.mock("../../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import { mount } from "enzyme";
import { ErrorHandling } from "../error_handling";
import { ErrorHandlingProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { edit, save } from "../../../../api/crud";
import { fakeState } from "../../../../__test_support__/fake_state";
import {
fakeFirmwareConfig
} from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex
} from "../../../../__test_support__/resource_index_builder";
describe("<ErrorHandling />", () => {
const fakeConfig = fakeFirmwareConfig();
const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]);
const fakeProps = (): ErrorHandlingProps => ({
dispatch: jest.fn(x => x(jest.fn(), () => state)),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
});
it("shows error handling labels", () => {
const p = fakeProps();
const wrapper = mount(<ErrorHandling {...p} />);
expect(wrapper.text().toLowerCase()).toContain("error handling");
});
it("toggles retries e-stop parameter", () => {
const p = fakeProps();
p.controlPanelState.error_handling = true;
p.sourceFwConfig = () => ({ value: 1, consistent: true });
const wrapper = mount(<ErrorHandling {...p} />);
wrapper.find("button").at(0).simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { param_e_stop_on_mov_err: 0 });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
});

View File

@ -1,16 +1,17 @@
import * as React from "react"; import * as React from "react";
import { Header } from "../header"; import { Header } from "../header";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { DeviceSetting } from "../../../../constants";
describe("<Header/>", () => { describe("<Header/>", () => {
it("renders", () => { it("renders", () => {
const fn = jest.fn(); const fn = jest.fn();
const el = mount(<Header const el = mount(<Header
title="FOO" title={DeviceSetting.motors}
expanded={true} expanded={true}
name={"motors"} panel={"motors"}
dispatch={fn} />); dispatch={fn} />);
expect(el.text()).toContain("FOO"); expect(el.text().toLowerCase()).toContain("motors");
expect(el.find(".fa-minus").length).toBe(1); expect(el.find(".fa-minus").length).toBe(1);
}); });
}); });

View File

@ -1,7 +1,17 @@
jest.mock("../../../actions", () => ({ updateMCU: jest.fn() })); jest.mock("../../../actions", () => ({
updateMCU: jest.fn(),
commandErr: jest.fn(),
}));
const mockDevice = {
calibrate: jest.fn(() => Promise.resolve({})),
findHome: jest.fn(() => Promise.resolve({})),
setZero: jest.fn(() => Promise.resolve({})),
};
jest.mock("../../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react"; import * as React from "react";
import { mount } from "enzyme"; import { mount, shallow } from "enzyme";
import { HomingAndCalibration } from "../homing_and_calibration"; import { HomingAndCalibration } from "../homing_and_calibration";
import { bot } from "../../../../__test_support__/fake_state/bot"; import { bot } from "../../../../__test_support__/fake_state/bot";
import { updateMCU } from "../../../actions"; import { updateMCU } from "../../../actions";
@ -10,20 +20,28 @@ import {
} from "../../../../__test_support__/fake_state/resources"; } from "../../../../__test_support__/fake_state/resources";
import { error, warning } from "../../../../toast/toast"; import { error, warning } from "../../../../toast/toast";
import { inputEvent } from "../../../../__test_support__/fake_html_events"; import { inputEvent } from "../../../../__test_support__/fake_html_events";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { HomingAndCalibrationProps } from "../../interfaces";
import { CalibrationRow } from "../calibration_row";
describe("<HomingAndCalibration />", () => { describe("<HomingAndCalibration />", () => {
const fakeProps = (): HomingAndCalibrationProps => ({
dispatch: jest.fn(),
bot,
controlPanelState: panelState(),
sourceFwConfig: x => ({
value: bot.hardware.mcu_params[x], consistent: true
}),
firmwareConfig: fakeFirmwareConfig().body,
botDisconnected: false,
firmwareHardware: undefined,
});
function testAxisLengthInput( function testAxisLengthInput(
provided: string, expected: string | undefined) { provided: string, expected: string | undefined) {
const dispatch = jest.fn(); const p = fakeProps();
bot.controlPanelState.homing_and_calibration = true; p.bot.controlPanelState.homing_and_calibration = true;
const result = mount(<HomingAndCalibration const result = mount(<HomingAndCalibration {...p} />);
dispatch={dispatch}
bot={bot}
firmwareConfig={fakeFirmwareConfig().body}
sourceFwConfig={x => ({
value: bot.hardware.mcu_params[x], consistent: true
})}
botDisconnected={false} />);
const e = inputEvent(provided); const e = inputEvent(provided);
const input = result.find("input").first().props(); const input = result.find("input").first().props();
input.onChange && input.onChange(e); input.onChange && input.onChange(e);
@ -45,4 +63,33 @@ describe("<HomingAndCalibration />", () => {
expect(warning).not.toHaveBeenCalled(); expect(warning).not.toHaveBeenCalled();
expect(error).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled();
}); });
it("finds home", () => {
const wrapper = shallow(<HomingAndCalibration {...fakeProps()} />);
wrapper.find(CalibrationRow).first().props().action("x");
expect(mockDevice.findHome).toHaveBeenCalledWith({
axis: "x", speed: 100
});
});
it("calibrates", () => {
const wrapper = shallow(<HomingAndCalibration {...fakeProps()} />);
wrapper.find(CalibrationRow).at(1).props().action("all");
expect(mockDevice.calibrate).toHaveBeenCalledWith({ axis: "all" });
});
it("sets zero", () => {
const wrapper = shallow(<HomingAndCalibration {...fakeProps()} />);
wrapper.find(CalibrationRow).last().props().action("all");
expect(mockDevice.setZero).toHaveBeenCalledWith("all");
});
it("shows express board related labels", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
p.controlPanelState.homing_and_calibration = true;
const wrapper = shallow(<HomingAndCalibration {...p} />);
expect(wrapper.find(CalibrationRow).first().props().toolTip)
.toContain("stall detection");
});
}); });

View File

@ -1,33 +0,0 @@
const mockDevice = {
findHome: jest.fn(() => Promise.resolve({}))
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { mount } from "enzyme";
import { HomingRow } from "../homing_row";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<HomingRow />", () => {
it("renders three buttons", () => {
const wrapper = mount(<HomingRow
hardware={bot.hardware.mcu_params}
botDisconnected={false} />);
const txt = wrapper.text().toUpperCase();
["X", "Y", "Z"].map(function (axis) {
expect(txt).toContain(`HOME ${axis}`);
});
});
it("calls device", () => {
const result = mount(<HomingRow
hardware={bot.hardware.mcu_params}
botDisconnected={false} />);
[0, 1, 2].map(i =>
result.find("LockableButton").at(i).simulate("click"));
[{ axis: "x", speed: 100 }, { axis: "y", speed: 100 }].map(x =>
expect(mockDevice.findHome).toHaveBeenCalledWith(x));
});
});

View File

@ -37,8 +37,6 @@ describe("<Motors/>", () => {
it("renders the base case", () => { it("renders the base case", () => {
const wrapper = render(<Motors {...fakeProps()} />); const wrapper = render(<Motors {...fakeProps()} />);
["Enable 2nd X Motor", ["Enable 2nd X Motor",
"Max Retries",
"E-Stop on Movement Error",
"Max Speed (mm/s)" "Max Speed (mm/s)"
].map(string => ].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
@ -48,16 +46,14 @@ describe("<Motors/>", () => {
const p = fakeProps(); const p = fakeProps();
p.firmwareHardware = "express_k10"; p.firmwareHardware = "express_k10";
const wrapper = render(<Motors {...p} />); const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).toContain("Stall"); expect(wrapper.text()).toContain("Motor Current");
expect(wrapper.text()).toContain("Current");
}); });
it("doesn't show TMC parameters", () => { it("doesn't show TMC parameters", () => {
const p = fakeProps(); const p = fakeProps();
p.firmwareHardware = "farmduino"; p.firmwareHardware = "farmduino";
const wrapper = render(<Motors {...p} />); const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).not.toContain("Stall"); expect(wrapper.text()).not.toContain("Motor Current");
expect(wrapper.text()).not.toContain("Current");
}); });
const testParamToggle = ( const testParamToggle = (
@ -72,15 +68,6 @@ describe("<Motors/>", () => {
expect(save).toHaveBeenCalledWith(fakeConfig.uuid); expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
}); });
}; };
testParamToggle("toggles retries e-stop parameter", "param_e_stop_on_mov_err", 0); testParamToggle("toggles enable X2", "movement_secondary_motor_x", 6);
testParamToggle("toggles enable X2", "movement_secondary_motor_x", 7); testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 7);
testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 8);
it("renders TMC params", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).toContain("Motor Current");
expect(wrapper.text()).toContain("Stall Sensitivity");
});
}); });

View File

@ -0,0 +1,22 @@
import * as React from "react";
import { mount } from "enzyme";
import { PinBindings } from "../pin_bindings";
import { PinBindingsProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import {
buildResourceIndex
} from "../../../../__test_support__/resource_index_builder";
describe("<PinBindings />", () => {
const fakeProps = (): PinBindingsProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
resources: buildResourceIndex([]).index,
});
it("shows pin binding labels", () => {
const p = fakeProps();
const wrapper = mount(<PinBindings {...p} />);
expect(wrapper.text().toLowerCase()).toContain("pin bindings");
});
});

View File

@ -1,19 +0,0 @@
const mockDevice = {
setZero: jest.fn(() => Promise.resolve())
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { mount } from "enzyme";
import { ZeroRow } from "../zero_row";
describe("<HomingRow />", () => {
it("calls device", () => {
const result = mount(<ZeroRow botDisconnected={false} />);
[0, 1, 2].map(i => result.find("ZeroButton").at(i).simulate("click"));
["x", "y", "z"].map(x =>
expect(mockDevice.setZero).toHaveBeenCalledWith(x));
expect(mockDevice.setZero).toHaveBeenCalledTimes(3);
});
});

View File

@ -1,40 +1,37 @@
import * as React from "react"; import * as React from "react";
import { getDevice } from "../../../device";
import { Axis } from "../../interfaces";
import { LockableButton } from "../lockable_button"; import { LockableButton } from "../lockable_button";
import { axisTrackingStatus } from "../axis_tracking_status"; import { axisTrackingStatus } from "../axis_tracking_status";
import { ToolTips } from "../../../constants";
import { Row, Col, Help } from "../../../ui/index"; import { Row, Col, Help } from "../../../ui/index";
import { CalibrationRowProps } from "../interfaces"; import { CalibrationRowProps } from "../interfaces";
import { commandErr } from "../../actions";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { Highlight } from "../maybe_highlight";
const calibrate = (axis: Axis) => getDevice()
.calibrate({ axis })
.catch(commandErr("Calibration"));
export function CalibrationRow(props: CalibrationRowProps) { export function CalibrationRow(props: CalibrationRowProps) {
const { hardware, botDisconnected } = props; const { hardware, botDisconnected } = props;
return <Row> return <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={props.title}>
<label> <Col xs={6} className={"widget-body-tooltips"}>
{t("CALIBRATION")} <label>
</label> {t(props.title)}
<Help text={ToolTips.CALIBRATION} requireClick={true} position={Position.RIGHT} /> </label>
</Col> <Help text={t(props.toolTip)}
{axisTrackingStatus(hardware) requireClick={true} position={Position.RIGHT} />
.map(row => { </Col>
const { axis, disabled } = row; {axisTrackingStatus(hardware)
return <Col xs={2} key={axis} className={"centered-button-div"}> .map(row => {
<LockableButton const { axis } = row;
disabled={disabled || botDisconnected} const hardwareDisabled = props.type == "zero" ? false : row.disabled;
onClick={() => calibrate(axis)}> return <Col xs={2} key={axis} className={"centered-button-div"}>
{t("CALIBRATE {{axis}}", { axis })} <LockableButton
</LockableButton> disabled={hardwareDisabled || botDisconnected}
</Col>; onClick={() => props.action(axis)}>
})} {`${t(props.axisTitle)} ${axis}`}
</LockableButton>
</Col>;
})}
</Highlight>
</Row>; </Row>;
} }

View File

@ -3,41 +3,45 @@ import { DangerZoneProps } from "../interfaces";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { Content } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function DangerZone(props: DangerZoneProps) { export function DangerZone(props: DangerZoneProps) {
const { dispatch, onReset, botDisconnected } = props; const { dispatch, onReset, botDisconnected } = props;
const { danger_zone } = props.controlPanelState; const { danger_zone } = props.controlPanelState;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.dangerZone}>
<Header <Header
expanded={danger_zone} expanded={danger_zone}
title={t("Danger Zone")} title={DeviceSetting.dangerZone}
name={"danger_zone"} panel={"danger_zone"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!danger_zone}> <Collapse isOpen={!!danger_zone}>
<Row> <Row>
<Col xs={4}> <Highlight settingName={DeviceSetting.resetHardwareParams}>
<label> <Col xs={4}>
{t("Reset hardware parameter defaults")} <label>
</label> {t(DeviceSetting.resetHardwareParams)}
</Col> </label>
<Col xs={6}> </Col>
<p> <Col xs={6}>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} <p>
</p> {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</Col> </p>
<Col xs={2} className={"centered-button-div"}> </Col>
<button <Col xs={2} className={"centered-button-div"}>
className="fb-button red" <button
disabled={botDisconnected} className="fb-button red"
onClick={onReset}> disabled={botDisconnected}
{t("RESET")} onClick={onReset}>
</button> {t("RESET")}
</Col> </button>
</Col>
</Highlight>
</Row> </Row>
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -0,0 +1,111 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips, DeviceSetting } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { isExpressBoard } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
export function Encoders(props: EncodersProps) {
const { encoders } = props.controlPanelState;
const { dispatch, sourceFwConfig, firmwareHardware } = props;
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value,
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value
};
const isExpress = isExpressBoard(firmwareHardware);
return <Highlight className={"section"}
settingName={DeviceSetting.encoders}>
<Header
expanded={encoders}
title={isExpress
? DeviceSetting.stallDetection
: DeviceSetting.encoders}
panel={"encoders"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup
label={isExpress
? DeviceSetting.enableStallDetection
: DeviceSetting.enableEncoders}
tooltip={isExpress
? ToolTips.ENABLE_STALL_DETECTION
: ToolTips.ENABLE_ENCODERS}
x={"encoder_enabled_x"}
y={"encoder_enabled_y"}
z={"encoder_enabled_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
{isExpress &&
<NumericMCUInputGroup
label={DeviceSetting.stallSensitivity}
tooltip={ToolTips.STALL_SENSITIVITY}
x={"movement_stall_sensitivity_x"}
y={"movement_stall_sensitivity_y"}
z={"movement_stall_sensitivity_z"}
gray={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
<BooleanMCUInputGroup
label={DeviceSetting.useEncodersForPositioning}
tooltip={ToolTips.ENCODER_POSITIONING}
x={"encoder_use_for_pos_x"}
y={"encoder_use_for_pos_y"}
z={"encoder_use_for_pos_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
<BooleanMCUInputGroup
label={DeviceSetting.invertEncoders}
tooltip={ToolTips.INVERT_ENCODERS}
x={"encoder_invert_x"}
y={"encoder_invert_y"}
z={"encoder_invert_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
label={DeviceSetting.maxMissedSteps}
tooltip={isExpress
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
: ToolTips.MAX_MISSED_STEPS_ENCODERS}
x={"encoder_missed_steps_max_x"}
y={"encoder_missed_steps_max_y"}
z={"encoder_missed_steps_max_z"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
label={DeviceSetting.missedStepDecay}
tooltip={ToolTips.MISSED_STEP_DECAY}
x={"encoder_missed_steps_decay_x"}
y={"encoder_missed_steps_decay_y"}
z={"encoder_missed_steps_decay_z"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
{!isExpress &&
<NumericMCUInputGroup
label={DeviceSetting.encoderScaling}
tooltip={ToolTips.ENCODER_SCALING}
x={"encoder_scaling_x"}
y={"encoder_scaling_y"}
z={"encoder_scaling_z"}
xScale={sourceFwConfig("movement_microsteps_x").value}
yScale={sourceFwConfig("movement_microsteps_y").value}
zScale={sourceFwConfig("movement_microsteps_z").value}
intSize={"long"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />}
</Collapse>
</Highlight>;
}

View File

@ -1,130 +0,0 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { Feature } from "../../interfaces";
import { t } from "../../../i18next_wrapper";
import { isExpressBoard } from "../firmware_hardware_support";
export function EncodersAndEndStops(props: EncodersProps) {
const { encoders_and_endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props;
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value,
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value
};
return <section>
<Header
expanded={encoders_and_endstops}
title={isExpressBoard(firmwareHardware)
? t("Stall Detection and Endstops")
: t("Encoders and Endstops")}
name={"encoders_and_endstops"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders_and_endstops}>
<BooleanMCUInputGroup
name={isExpressBoard(firmwareHardware)
? t("Enable Stall Detection")
: t("Enable Encoders")}
tooltip={ToolTips.ENABLE_ENCODERS}
x={"encoder_enabled_x"}
y={"encoder_enabled_y"}
z={"encoder_enabled_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
{!isExpressBoard(firmwareHardware) &&
<BooleanMCUInputGroup
name={t("Use Encoders for Positioning")}
tooltip={ToolTips.ENCODER_POSITIONING}
x={"encoder_use_for_pos_x"}
y={"encoder_use_for_pos_y"}
z={"encoder_use_for_pos_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpressBoard(firmwareHardware) &&
<BooleanMCUInputGroup
name={t("Invert Encoders")}
tooltip={ToolTips.INVERT_ENCODERS}
x={"encoder_invert_x"}
y={"encoder_invert_y"}
z={"encoder_invert_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
name={t("Max Missed Steps")}
tooltip={ToolTips.MAX_MISSED_STEPS}
x={"encoder_missed_steps_max_x"}
y={"encoder_missed_steps_max_y"}
z={"encoder_missed_steps_max_z"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Missed Step Decay")}
tooltip={ToolTips.ENCODER_MISSED_STEP_DECAY}
x={"encoder_missed_steps_decay_x"}
y={"encoder_missed_steps_decay_y"}
z={"encoder_missed_steps_decay_z"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
{!isExpressBoard(firmwareHardware) &&
<NumericMCUInputGroup
name={t("Encoder Scaling")}
tooltip={ToolTips.ENCODER_SCALING}
x={"encoder_scaling_x"}
y={"encoder_scaling_y"}
z={"encoder_scaling_z"}
xScale={sourceFwConfig("movement_microsteps_x").value}
yScale={sourceFwConfig("movement_microsteps_y").value}
zScale={sourceFwConfig("movement_microsteps_z").value}
intSize={shouldDisplay(Feature.long_scaling_factor) ? "long" : "short"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />}
<BooleanMCUInputGroup
name={t("Enable Endstops")}
tooltip={ToolTips.ENABLE_ENDSTOPS}
x={"movement_enable_endpoints_x"}
y={"movement_enable_endpoints_y"}
z={"movement_enable_endpoints_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Swap Endstops")}
tooltip={ToolTips.SWAP_ENDPOINTS}
x={"movement_invert_endpoints_x"}
y={"movement_invert_endpoints_y"}
z={"movement_invert_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Invert Endstops")}
tooltip={ToolTips.INVERT_ENDPOINTS}
x={"movement_invert_2_endpoints_x"}
y={"movement_invert_2_endpoints_y"}
z={"movement_invert_2_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
</Collapse>
</section>;
}

View File

@ -0,0 +1,58 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips, DeviceSetting } from "../../../constants";
import { EndStopsProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { Highlight } from "../maybe_highlight";
export function EndStops(props: EndStopsProps) {
const { endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
return <Highlight className={"section"}
settingName={DeviceSetting.endstops}>
<Header
expanded={endstops}
title={DeviceSetting.endstops}
panel={"endstops"}
dispatch={dispatch} />
<Collapse isOpen={!!endstops}>
<BooleanMCUInputGroup
label={DeviceSetting.enableEndstops}
tooltip={ToolTips.ENABLE_ENDSTOPS}
x={"movement_enable_endpoints_x"}
y={"movement_enable_endpoints_y"}
z={"movement_enable_endpoints_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
label={DeviceSetting.swapEndstops}
tooltip={ToolTips.SWAP_ENDPOINTS}
x={"movement_invert_endpoints_x"}
y={"movement_invert_endpoints_y"}
z={"movement_invert_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
label={DeviceSetting.invertEndstops}
tooltip={ToolTips.INVERT_ENDPOINTS}
x={"movement_invert_2_endpoints_x"}
y={"movement_invert_2_endpoints_y"}
z={"movement_invert_2_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
</Collapse>
</Highlight>;
}

View File

@ -0,0 +1,54 @@
import * as React from "react";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { ToolTips, DeviceSetting } from "../../../constants";
import { ErrorHandlingProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { settingToggle } from "../../actions";
import { SingleSettingRow } from "./single_setting_row";
import { ToggleButton } from "../../../controls/toggle_button";
import { Highlight } from "../maybe_highlight";
export function ErrorHandling(props: ErrorHandlingProps) {
const { error_handling } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
return <Highlight className={"section"}
settingName={DeviceSetting.errorHandling}>
<Header
expanded={error_handling}
title={DeviceSetting.errorHandling}
panel={"error_handling"}
dispatch={dispatch} />
<Collapse isOpen={!!error_handling}>
<NumericMCUInputGroup
label={DeviceSetting.timeoutAfter}
tooltip={ToolTips.TIMEOUT_AFTER}
x={"movement_timeout_x"}
y={"movement_timeout_y"}
z={"movement_timeout_z"}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<SingleSettingRow settingType="input"
label={DeviceSetting.maxRetries}
tooltip={ToolTips.MAX_MOVEMENT_RETRIES}>
<McuInputBox
setting="param_mov_nr_retry"
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</SingleSettingRow>
<SingleSettingRow settingType="button"
label={DeviceSetting.estopOnMovementError}
tooltip={ToolTips.E_STOP_ON_MOV_ERR}>
<ToggleButton
toggleValue={eStopOnMoveError.value}
dim={!eStopOnMoveError.consistent}
toggleAction={() => dispatch(
settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} />
</SingleSettingRow>
</Collapse>
</Highlight>;
}

View File

@ -25,7 +25,7 @@ const getSubKeyName = (key: string) => {
}; };
export const FwParamExportMenu = export const FwParamExportMenu =
({ firmwareConfig }: { firmwareConfig: FirmwareConfig }) => { ({ firmwareConfig }: { firmwareConfig: FirmwareConfig | undefined }) => {
/** Filter out unnecessary parameters. */ /** Filter out unnecessary parameters. */
const filteredConfig = pickBy(firmwareConfig, (_, key) => const filteredConfig = pickBy(firmwareConfig, (_, key) =>
!["id", "device_id", "api_migrated", "created_at", "updated_at", !["id", "device_id", "api_migrated", "created_at", "updated_at",

View File

@ -2,18 +2,20 @@ import * as React from "react";
import { ControlPanelState } from "../../interfaces"; import { ControlPanelState } from "../../interfaces";
import { toggleControlPanel } from "../../actions"; import { toggleControlPanel } from "../../actions";
import { ExpandableHeader } from "../../../ui/expandable_header"; import { ExpandableHeader } from "../../../ui/expandable_header";
import { t } from "../../../i18next_wrapper";
import { DeviceSetting } from "../../../constants";
interface Props { interface Props {
dispatch: Function; dispatch: Function;
name: keyof ControlPanelState; panel: keyof ControlPanelState;
title: string; title: DeviceSetting;
expanded: boolean; expanded: boolean;
} }
export const Header = (props: Props) => { export const Header = (props: Props) => {
const { dispatch, name, title, expanded } = props; const { dispatch, panel, title, expanded } = props;
return <ExpandableHeader return <ExpandableHeader
expanded={expanded} expanded={expanded}
title={title} title={t(title)}
onClick={() => dispatch(toggleControlPanel(name))} />; onClick={() => dispatch(toggleControlPanel(panel))} />;
}; };

View File

@ -1,20 +1,25 @@
import * as React from "react"; import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { HomingRow } from "./homing_row";
import { CalibrationRow } from "./calibration_row"; import { CalibrationRow } from "./calibration_row";
import { ZeroRow } from "./zero_row";
import { disabledAxisMap } from "../axis_tracking_status"; import { disabledAxisMap } from "../axis_tracking_status";
import { HomingAndCalibrationProps } from "../interfaces"; import { HomingAndCalibrationProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { calculateScale } from "./motors"; import { calculateScale } from "./motors";
import { isExpressBoard } from "../firmware_hardware_support";
import { getDevice } from "../../../device";
import { commandErr } from "../../actions";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
import { Highlight } from "../maybe_highlight";
export function HomingAndCalibration(props: HomingAndCalibrationProps) { export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const { dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected const {
dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected,
firmwareHardware
} = props; } = props;
const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params; const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params;
const { homing_and_calibration } = props.bot.controlPanelState; const { homing_and_calibration } = props.bot.controlPanelState;
@ -27,19 +32,51 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const scale = calculateScale(sourceFwConfig); const scale = calculateScale(sourceFwConfig);
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.homingAndCalibration}>
<Header <Header
title={t("Homing and Calibration")} title={DeviceSetting.homingAndCalibration}
name={"homing_and_calibration"} panel={"homing_and_calibration"}
dispatch={dispatch} dispatch={dispatch}
expanded={homing_and_calibration} /> expanded={homing_and_calibration} />
<Collapse isOpen={!!homing_and_calibration}> <Collapse isOpen={!!homing_and_calibration}>
<HomingRow hardware={hardware} botDisconnected={botDisconnected} /> <CalibrationRow
<CalibrationRow hardware={hardware} botDisconnected={botDisconnected} /> type={"find_home"}
<ZeroRow botDisconnected={botDisconnected} /> title={DeviceSetting.homing}
axisTitle={t("FIND HOME")}
toolTip={isExpressBoard(firmwareHardware)
? ToolTips.HOMING_STALL_DETECTION
: ToolTips.HOMING_ENCODERS}
action={axis => getDevice()
.findHome({ speed: CONFIG_DEFAULTS.speed, axis })
.catch(commandErr("'Find Home' request"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<CalibrationRow
type={"calibrate"}
title={DeviceSetting.calibration}
axisTitle={t("CALIBRATE")}
toolTip={isExpressBoard(firmwareHardware)
? ToolTips.CALIBRATION_STALL_DETECTION
: ToolTips.CALIBRATION_ENCODERS}
action={axis => getDevice().calibrate({ axis })
.catch(commandErr("Calibration"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<CalibrationRow
type={"zero"}
title={DeviceSetting.setZeroPosition}
axisTitle={t("ZERO")}
toolTip={ToolTips.SET_ZERO_POSITION}
action={axis => getDevice().setZero(axis)
.catch(commandErr("Zeroing"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Find Home on Boot")} label={DeviceSetting.findHomeOnBoot}
tooltip={ToolTips.FIND_HOME_ON_BOOT} tooltip={isExpressBoard(firmwareHardware)
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
disable={disabled} disable={disabled}
x={"movement_home_at_boot_x"} x={"movement_home_at_boot_x"}
y={"movement_home_at_boot_y"} y={"movement_home_at_boot_y"}
@ -48,7 +85,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
caution={true} /> caution={true} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Stop at Home")} label={DeviceSetting.stopAtHome}
tooltip={ToolTips.STOP_AT_HOME} tooltip={ToolTips.STOP_AT_HOME}
x={"movement_stop_at_home_x"} x={"movement_stop_at_home_x"}
y={"movement_stop_at_home_y"} y={"movement_stop_at_home_y"}
@ -56,7 +93,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Stop at Max")} label={DeviceSetting.stopAtMax}
tooltip={ToolTips.STOP_AT_MAX} tooltip={ToolTips.STOP_AT_MAX}
x={"movement_stop_at_max_x"} x={"movement_stop_at_max_x"}
y={"movement_stop_at_max_y"} y={"movement_stop_at_max_y"}
@ -64,7 +101,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Negative Coordinates Only")} label={DeviceSetting.negativeCoordinatesOnly}
tooltip={ToolTips.NEGATIVE_COORDINATES_ONLY} tooltip={ToolTips.NEGATIVE_COORDINATES_ONLY}
x={"movement_home_up_x"} x={"movement_home_up_x"}
y={"movement_home_up_y"} y={"movement_home_up_y"}
@ -72,7 +109,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Axis Length (mm)")} label={DeviceSetting.axisLength}
tooltip={ToolTips.LENGTH} tooltip={ToolTips.LENGTH}
x={"movement_axis_nr_steps_x"} x={"movement_axis_nr_steps_x"}
y={"movement_axis_nr_steps_y"} y={"movement_axis_nr_steps_y"}
@ -88,14 +125,6 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} dispatch={dispatch}
intSize={"long"} /> intSize={"long"} />
<NumericMCUInputGroup
name={t("Timeout after (seconds)")}
tooltip={ToolTips.TIMEOUT_AFTER}
x={"movement_timeout_x"}
y={"movement_timeout_y"}
z={"movement_timeout_z"}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -1,41 +0,0 @@
import * as React from "react";
import { HomingRowProps } from "../interfaces";
import { LockableButton } from "../lockable_button";
import { axisTrackingStatus } from "../axis_tracking_status";
import { ToolTips } from "../../../constants";
import { Row, Col, Help } from "../../../ui/index";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
import { commandErr } from "../../actions";
import { Axis } from "../../interfaces";
import { getDevice } from "../../../device";
import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
const speed = CONFIG_DEFAULTS.speed;
const findHome = (axis: Axis) => getDevice()
.findHome({ speed, axis })
.catch(commandErr("'Find Home' request"));
export function HomingRow(props: HomingRowProps) {
const { hardware, botDisconnected } = props;
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t("HOMING")}
</label>
<Help text={ToolTips.HOMING} requireClick={true} position={Position.RIGHT}/>
</Col>
{axisTrackingStatus(hardware)
.map((row) => {
const { axis, disabled } = row;
return <Col xs={2} key={axis} className={"centered-button-div"}>
<LockableButton
disabled={disabled || botDisconnected}
onClick={() => findHome(axis)}>
{t("FIND HOME {{axis}}", { axis })}
</LockableButton>
</Col>;
})}
</Row>;
}

View File

@ -1,36 +1,18 @@
import * as React from "react"; import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group"; import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { ToggleButton } from "../../../controls/toggle_button"; import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions"; import { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group"; import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { MotorsProps } from "../interfaces"; import { MotorsProps } from "../interfaces";
import { Row, Col, Help } from "../../../ui/index";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse, Position } from "@blueprintjs/core"; import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { t } from "../../../i18next_wrapper";
import { Xyz, McuParamName } from "farmbot"; import { Xyz, McuParamName } from "farmbot";
import { SourceFwConfig } from "../../interfaces"; import { SourceFwConfig } from "../../interfaces";
import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props"; import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props";
import { isTMCBoard, isExpressBoard } from "../firmware_hardware_support"; import { isTMCBoard } from "../firmware_hardware_support";
import { SingleSettingRow } from "./single_setting_row";
const SingleSettingRow = import { Highlight } from "../maybe_highlight";
({ label, tooltip, settingType, children }: {
label: string,
tooltip: string,
children: React.ReactChild,
settingType: "button" | "input",
}) =>
<Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>{label}</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
{settingType === "button"
? <Col xs={2} className={"centered-button-div"}>{children}</Col>
: <Col xs={6}>{children}</Col>}
</Row>;
export const calculateScale = export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => { (sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
@ -51,39 +33,18 @@ export function Motors(props: MotorsProps) {
} = props; } = props;
const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x"); const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x");
const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x");
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
const scale = calculateScale(sourceFwConfig); const scale = calculateScale(sourceFwConfig);
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value, return <Highlight className={"section"}
y: !sourceFwConfig("encoder_enabled_y").value, settingName={DeviceSetting.motors}>
z: !sourceFwConfig("encoder_enabled_z").value,
};
return <section>
<Header <Header
expanded={controlPanelState.motors} expanded={controlPanelState.motors}
title={t("Motors")} title={DeviceSetting.motors}
name={"motors"} panel={"motors"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!controlPanelState.motors}> <Collapse isOpen={!!controlPanelState.motors}>
<SingleSettingRow settingType="input"
label={t("Max Retries")}
tooltip={ToolTips.MAX_MOVEMENT_RETRIES}>
<McuInputBox
setting="param_mov_nr_retry"
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</SingleSettingRow>
<SingleSettingRow settingType="button"
label={t("E-Stop on Movement Error")}
tooltip={ToolTips.E_STOP_ON_MOV_ERR}>
<ToggleButton
toggleValue={eStopOnMoveError.value}
dim={!eStopOnMoveError.consistent}
toggleAction={() => dispatch(
settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} />
</SingleSettingRow>
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Max Speed (mm/s)")} label={DeviceSetting.maxSpeed}
tooltip={ToolTips.MAX_SPEED} tooltip={ToolTips.MAX_SPEED}
x={"movement_max_spd_x"} x={"movement_max_spd_x"}
y={"movement_max_spd_y"} y={"movement_max_spd_y"}
@ -94,7 +55,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Homing Speed (mm/s)")} label={DeviceSetting.homingSpeed}
tooltip={ToolTips.HOME_SPEED} tooltip={ToolTips.HOME_SPEED}
x={"movement_home_spd_x"} x={"movement_home_spd_x"}
y={"movement_home_spd_y"} y={"movement_home_spd_y"}
@ -105,7 +66,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Minimum Speed (mm/s)")} label={DeviceSetting.minimumSpeed}
tooltip={ToolTips.MIN_SPEED} tooltip={ToolTips.MIN_SPEED}
x={"movement_min_spd_x"} x={"movement_min_spd_x"}
y={"movement_min_spd_y"} y={"movement_min_spd_y"}
@ -116,7 +77,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Accelerate for (mm)")} label={DeviceSetting.accelerateFor}
tooltip={ToolTips.ACCELERATE_FOR} tooltip={ToolTips.ACCELERATE_FOR}
x={"movement_steps_acc_dec_x"} x={"movement_steps_acc_dec_x"}
y={"movement_steps_acc_dec_y"} y={"movement_steps_acc_dec_y"}
@ -127,7 +88,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} /> dispatch={dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Steps per MM")} label={DeviceSetting.stepsPerMm}
tooltip={ToolTips.STEPS_PER_MM} tooltip={ToolTips.STEPS_PER_MM}
x={"movement_step_per_mm_x"} x={"movement_step_per_mm_x"}
y={"movement_step_per_mm_y"} y={"movement_step_per_mm_y"}
@ -139,7 +100,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={props.sourceFwConfig} sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} /> dispatch={props.dispatch} />
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Microsteps per step")} label={DeviceSetting.microstepsPerStep}
tooltip={ToolTips.MICROSTEPS_PER_STEP} tooltip={ToolTips.MICROSTEPS_PER_STEP}
x={"movement_microsteps_x"} x={"movement_microsteps_x"}
y={"movement_microsteps_y"} y={"movement_microsteps_y"}
@ -147,7 +108,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={props.sourceFwConfig} sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} /> dispatch={props.dispatch} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Always Power Motors")} label={DeviceSetting.alwaysPowerMotors}
tooltip={ToolTips.ALWAYS_POWER_MOTORS} tooltip={ToolTips.ALWAYS_POWER_MOTORS}
x={"movement_keep_active_x"} x={"movement_keep_active_x"}
y={"movement_keep_active_y"} y={"movement_keep_active_y"}
@ -155,7 +116,7 @@ export function Motors(props: MotorsProps) {
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
name={t("Invert Motors")} label={DeviceSetting.invertMotors}
tooltip={ToolTips.INVERT_MOTORS} tooltip={ToolTips.INVERT_MOTORS}
x={"movement_invert_motor_x"} x={"movement_invert_motor_x"}
y={"movement_invert_motor_y"} y={"movement_invert_motor_y"}
@ -164,25 +125,15 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
{isTMCBoard(firmwareHardware) && {isTMCBoard(firmwareHardware) &&
<NumericMCUInputGroup <NumericMCUInputGroup
name={t("Motor Current")} label={DeviceSetting.motorCurrent}
tooltip={ToolTips.MOTOR_CURRENT} tooltip={ToolTips.MOTOR_CURRENT}
x={"movement_motor_current_x"} x={"movement_motor_current_x"}
y={"movement_motor_current_y"} y={"movement_motor_current_y"}
z={"movement_motor_current_z"} z={"movement_motor_current_z"}
dispatch={dispatch} dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />} sourceFwConfig={sourceFwConfig} />}
{isExpressBoard(firmwareHardware) &&
<NumericMCUInputGroup
name={t("Stall Sensitivity")}
tooltip={ToolTips.STALL_SENSITIVITY}
x={"movement_stall_sensitivity_x"}
y={"movement_stall_sensitivity_y"}
z={"movement_stall_sensitivity_z"}
gray={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
<SingleSettingRow settingType="button" <SingleSettingRow settingType="button"
label={t("Enable 2nd X Motor")} label={DeviceSetting.enable2ndXMotor}
tooltip={ToolTips.ENABLE_X2_MOTOR}> tooltip={ToolTips.ENABLE_X2_MOTOR}>
<ToggleButton <ToggleButton
toggleValue={enable2ndXMotor.value} toggleValue={enable2ndXMotor.value}
@ -191,7 +142,7 @@ export function Motors(props: MotorsProps) {
settingToggle("movement_secondary_motor_x", sourceFwConfig))} /> settingToggle("movement_secondary_motor_x", sourceFwConfig))} />
</SingleSettingRow> </SingleSettingRow>
<SingleSettingRow settingType="button" <SingleSettingRow settingType="button"
label={t("Invert 2nd X Motor")} label={DeviceSetting.invert2ndXMotor}
tooltip={ToolTips.INVERT_MOTORS}> tooltip={ToolTips.INVERT_MOTORS}>
<ToggleButton <ToggleButton
grayscale={!enable2ndXMotor.value} grayscale={!enable2ndXMotor.value}
@ -201,5 +152,5 @@ export function Motors(props: MotorsProps) {
settingToggle("movement_secondary_motor_invert_x", sourceFwConfig))} /> settingToggle("movement_secondary_motor_invert_x", sourceFwConfig))} />
</SingleSettingRow> </SingleSettingRow>
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { PinBindingsProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { PinBindingsContent } from "../../pin_bindings/pin_bindings";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props;
return <Highlight className={"section"}
settingName={DeviceSetting.pinBindings}>
<Header
expanded={pin_bindings}
title={DeviceSetting.pinBindings}
panel={"pin_bindings"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} />
</Collapse>
</Highlight>;
}

View File

@ -4,19 +4,21 @@ import { PinGuardProps } from "../interfaces";
import { Header } from "./header"; import { Header } from "./header";
import { Collapse, Position } from "@blueprintjs/core"; import { Collapse, Position } from "@blueprintjs/core";
import { Row, Col, Help } from "../../../ui/index"; import { Row, Col, Help } from "../../../ui/index";
import { ToolTips } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function PinGuard(props: PinGuardProps) { export function PinGuard(props: PinGuardProps) {
const { pin_guard } = props.controlPanelState; const { pin_guard } = props.controlPanelState;
const { dispatch, sourceFwConfig, resources } = props; const { dispatch, sourceFwConfig, resources } = props;
return <section> return <Highlight className={"section"}
settingName={DeviceSetting.pinGuard}>
<Header <Header
expanded={pin_guard} expanded={pin_guard}
title={t("Pin Guard")} title={DeviceSetting.pinGuard}
name={"pin_guard"} panel={"pin_guard"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!pin_guard}> <Collapse isOpen={!!pin_guard}>
<Row> <Row>
@ -79,5 +81,5 @@ export function PinGuard(props: PinGuardProps) {
resources={resources} resources={resources}
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
</Collapse> </Collapse>
</section>; </Highlight>;
} }

View File

@ -0,0 +1,27 @@
import * as React from "react";
import { Row, Col, Help } from "../../../ui/index";
import { Position } from "@blueprintjs/core";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
import { t } from "../../../i18next_wrapper";
export interface SingleSettingRowProps {
label: DeviceSetting;
tooltip: string;
children: React.ReactChild;
settingType: "button" | "input";
}
export const SingleSettingRow =
({ label, tooltip, settingType, children }: SingleSettingRowProps) =>
<Row>
<Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>{t(label)}</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
{settingType === "button"
? <Col xs={2} className={"centered-button-div"}>{children}</Col>
: <Col xs={6}>{children}</Col>}
</Highlight>
</Row>;

View File

@ -1,40 +0,0 @@
import * as React from "react";
import { getDevice } from "../../../device";
import { Axis } from "../../interfaces";
import { ToolTips } from "../../../constants";
import { Row, Col, Help } from "../../../ui/index";
import { ZeroRowProps } from "../interfaces";
import { commandErr } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
const zero =
(axis: Axis) => getDevice().setZero(axis).catch(commandErr("Zeroing"));
const AXES: Axis[] = ["x", "y", "z"];
export function ZeroButton(props: { axis: Axis; disabled: boolean; }) {
const { axis, disabled } = props;
return <button
className="fb-button yellow"
disabled={disabled}
onClick={() => zero(axis)}>
{t("zero {{axis}}", { axis })}
</button>;
}
export function ZeroRow({ botDisconnected }: ZeroRowProps) {
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t("SET ZERO POSITION")}
</label>
<Help text={ToolTips.SET_ZERO_POSITION} requireClick={true}
position={Position.RIGHT} />
</Col>
{AXES.map((axis) => {
return <Col xs={2} key={axis} className={"centered-button-div"}>
<ZeroButton axis={axis} disabled={botDisconnected} />
</Col>;
})}
</Row>;
}

View File

@ -1,16 +1,12 @@
import { import {
BotState, Xyz, SourceFwConfig, BotState, Xyz, SourceFwConfig,
ControlPanelState, ShouldDisplay ControlPanelState, Axis
} from "../interfaces"; } from "../interfaces";
import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist"; import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist";
import { IntegerSize } from "../../util"; import { IntegerSize } from "../../util";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { ResourceIndex } from "../../resources/interfaces"; import { ResourceIndex } from "../../resources/interfaces";
import { DeviceSetting } from "../../constants";
export interface HomingRowProps {
hardware: McuParams;
botDisconnected: boolean;
}
export interface ZeroRowProps { export interface ZeroRowProps {
botDisconnected: boolean; botDisconnected: boolean;
@ -19,16 +15,18 @@ export interface ZeroRowProps {
export interface HomingAndCalibrationProps { export interface HomingAndCalibrationProps {
dispatch: Function; dispatch: Function;
bot: BotState; bot: BotState;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
firmwareConfig: FirmwareConfig | undefined; firmwareConfig: FirmwareConfig | undefined;
botDisconnected: boolean; botDisconnected: boolean;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface BooleanMCUInputGroupProps { export interface BooleanMCUInputGroupProps {
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
dispatch: Function; dispatch: Function;
tooltip: string; tooltip: string;
name: string; label: DeviceSetting;
x: McuParamName; x: McuParamName;
y: McuParamName; y: McuParamName;
z: McuParamName; z: McuParamName;
@ -39,15 +37,20 @@ export interface BooleanMCUInputGroupProps {
} }
export interface CalibrationRowProps { export interface CalibrationRowProps {
type: "find_home" | "calibrate" | "zero";
hardware: McuParams; hardware: McuParams;
botDisconnected: boolean; botDisconnected: boolean;
action(axis: Axis): void;
toolTip: string;
title: DeviceSetting;
axisTitle: string;
} }
export interface NumericMCUInputGroupProps { export interface NumericMCUInputGroupProps {
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
dispatch: Function; dispatch: Function;
tooltip: string; tooltip: string;
name: string; label: DeviceSetting;
x: McuParamName; x: McuParamName;
xScale?: number; xScale?: number;
y: McuParamName; y: McuParamName;
@ -85,12 +88,29 @@ export interface MotorsProps {
export interface EncodersProps { export interface EncodersProps {
dispatch: Function; dispatch: Function;
shouldDisplay: ShouldDisplay;
controlPanelState: ControlPanelState; controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
firmwareHardware: FirmwareHardware | undefined; firmwareHardware: FirmwareHardware | undefined;
} }
export interface EndStopsProps {
dispatch: Function;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
}
export interface ErrorHandlingProps {
dispatch: Function;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
}
export interface PinBindingsProps {
dispatch: Function;
controlPanelState: ControlPanelState;
resources: ResourceIndex;
}
export interface DangerZoneProps { export interface DangerZoneProps {
dispatch: Function; dispatch: Function;
controlPanelState: ControlPanelState; controlPanelState: ControlPanelState;

View File

@ -0,0 +1,162 @@
import * as React from "react";
import { ControlPanelState } from "../interfaces";
import { toggleControlPanel } from "../actions";
import { urlFriendly } from "../../util";
import { DeviceSetting } from "../../constants";
const HOMING_PANEL = [
DeviceSetting.homingAndCalibration,
DeviceSetting.homing,
DeviceSetting.calibration,
DeviceSetting.setZeroPosition,
DeviceSetting.findHomeOnBoot,
DeviceSetting.stopAtHome,
DeviceSetting.stopAtMax,
DeviceSetting.negativeCoordinatesOnly,
DeviceSetting.axisLength,
];
const MOTORS_PANEL = [
DeviceSetting.motors,
DeviceSetting.maxSpeed,
DeviceSetting.homingSpeed,
DeviceSetting.minimumSpeed,
DeviceSetting.accelerateFor,
DeviceSetting.stepsPerMm,
DeviceSetting.microstepsPerStep,
DeviceSetting.alwaysPowerMotors,
DeviceSetting.invertMotors,
DeviceSetting.motorCurrent,
DeviceSetting.enable2ndXMotor,
DeviceSetting.invert2ndXMotor,
];
const ENCODERS_PANEL = [
DeviceSetting.encoders,
DeviceSetting.stallDetection,
DeviceSetting.enableEncoders,
DeviceSetting.enableStallDetection,
DeviceSetting.stallSensitivity,
DeviceSetting.useEncodersForPositioning,
DeviceSetting.invertEncoders,
DeviceSetting.maxMissedSteps,
DeviceSetting.missedStepDecay,
DeviceSetting.encoderScaling,
];
const ENDSTOPS_PANEL = [
DeviceSetting.endstops,
DeviceSetting.enableEndstops,
DeviceSetting.swapEndstops,
DeviceSetting.invertEndstops,
];
const ERROR_HANDLING_PANEL = [
DeviceSetting.errorHandling,
DeviceSetting.timeoutAfter,
DeviceSetting.maxRetries,
DeviceSetting.estopOnMovementError,
];
const PIN_GUARD_PANEL = [
DeviceSetting.pinGuard,
];
const DANGER_ZONE_PANEL = [
DeviceSetting.dangerZone,
DeviceSetting.resetHardwareParams,
];
const PIN_BINDINGS_PANEL = [
DeviceSetting.pinBindings,
];
const POWER_AND_RESET_PANEL = [
DeviceSetting.powerAndReset,
DeviceSetting.restartFarmbot,
DeviceSetting.shutdownFarmbot,
DeviceSetting.restartFirmware,
DeviceSetting.factoryReset,
DeviceSetting.autoFactoryReset,
DeviceSetting.connectionAttemptPeriod,
DeviceSetting.changeOwnership,
];
/** Look up parent panels for settings. */
const SETTING_PANEL_LOOKUP = {} as Record<DeviceSetting, keyof ControlPanelState>;
HOMING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "homing_and_calibration");
MOTORS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "motors");
ENCODERS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "encoders");
ENDSTOPS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "endstops");
ERROR_HANDLING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "error_handling");
PIN_GUARD_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_guard");
DANGER_ZONE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "danger_zone");
PIN_BINDINGS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_bindings");
POWER_AND_RESET_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "power_and_reset");
/** Look up parent panels for settings using URL-friendly names. */
const URL_FRIENDLY_LOOKUP: Record<string, keyof ControlPanelState> = {};
Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) =>
URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel);
/** Look up all relevant names for the same setting. */
const ALTERNATE_NAMES =
Object.values(DeviceSetting).reduce((acc, s) => { acc[s] = [s]; return acc; },
{} as Record<DeviceSetting, DeviceSetting[]>);
ALTERNATE_NAMES[DeviceSetting.encoders].push(DeviceSetting.stallDetection);
ALTERNATE_NAMES[DeviceSetting.stallDetection].push(DeviceSetting.encoders);
/** Generate array of names for the same setting. Most only have one. */
const compareValues = (settingName: DeviceSetting) =>
(ALTERNATE_NAMES[settingName]).map(s => urlFriendly(s));
/** Retrieve a highlight search term. */
const getHighlightName = () => location.search.split("?highlight=").pop();
/** Only open panel and highlight once per app load. Exported for tests. */
export const highlight = { opened: false, highlighted: false };
/** Open a panel if a setting in that panel is highlighted. */
export const maybeOpenPanel = (panelState: ControlPanelState) =>
(dispatch: Function) => {
if (highlight.opened) { return; }
const urlFriendlySettingName = urlFriendly(getHighlightName() || "");
if (!urlFriendlySettingName) { return; }
const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName];
const panelIsOpen = panelState[panel];
if (panelIsOpen) { return; }
dispatch(toggleControlPanel(panel));
highlight.opened = true;
};
/** Highlight a setting if provided as a search term. */
export const maybeHighlight = (settingName: DeviceSetting) => {
const item = getHighlightName();
if (highlight.highlighted || !item) { return ""; }
const isCurrentSetting = compareValues(settingName).includes(item);
if (!isCurrentSetting) { return ""; }
highlight.highlighted = true;
return "highlight";
};
export interface HighlightProps {
settingName: DeviceSetting;
children: React.ReactChild
| React.ReactChild[]
| (React.ReactChild | React.ReactChild[])[];
className?: string;
}
interface HighlightState {
className: string;
}
/** Wrap highlight-able settings. */
export class Highlight extends React.Component<HighlightProps, HighlightState> {
state: HighlightState = { className: maybeHighlight(this.props.settingName) };
componentDidMount = () => {
if (this.state.className == "highlight") {
/** Slowly fades highlight. */
this.setState({ className: "unhighlight" });
}
}
render() {
return <div className={`${this.props.className} ${this.state.className}`}>
{this.props.children}
</div>;
}
}

View File

@ -3,48 +3,52 @@ import { McuInputBox } from "./mcu_input_box";
import { NumericMCUInputGroupProps } from "./interfaces"; import { NumericMCUInputGroupProps } from "./interfaces";
import { Row, Col, Help } from "../../ui/index"; import { Row, Col, Help } from "../../ui/index";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { Highlight } from "./maybe_highlight";
import { t } from "../../i18next_wrapper";
export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) { export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
const { const {
sourceFwConfig, dispatch, tooltip, name, x, y, z, intSize, gray, float, sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float,
} = props; } = props;
return <Row> return <Row>
<Col xs={6} className={"widget-body-tooltips"}> <Highlight settingName={label}>
<label> <Col xs={6} className={"widget-body-tooltips"}>
{name} <label>
</label> {t(label)}
<Help text={tooltip} requireClick={true} position={Position.RIGHT} /> </label>
</Col> <Help text={tooltip} requireClick={true} position={Position.RIGHT} />
<Col xs={2}> </Col>
<McuInputBox <Col xs={2}>
setting={x} <McuInputBox
sourceFwConfig={sourceFwConfig} setting={x}
dispatch={dispatch} sourceFwConfig={sourceFwConfig}
intSize={intSize} dispatch={dispatch}
float={float} intSize={intSize}
scale={props.xScale} float={float}
gray={gray?.x} /> scale={props.xScale}
</Col> gray={gray?.x} />
<Col xs={2}> </Col>
<McuInputBox <Col xs={2}>
setting={y} <McuInputBox
sourceFwConfig={sourceFwConfig} setting={y}
dispatch={dispatch} sourceFwConfig={sourceFwConfig}
intSize={intSize} dispatch={dispatch}
float={float} intSize={intSize}
scale={props.yScale} float={float}
gray={gray?.y} /> scale={props.yScale}
</Col> gray={gray?.y} />
<Col xs={2}> </Col>
<McuInputBox <Col xs={2}>
setting={z} <McuInputBox
sourceFwConfig={sourceFwConfig} setting={z}
dispatch={dispatch} sourceFwConfig={sourceFwConfig}
intSize={intSize} dispatch={dispatch}
float={float} intSize={intSize}
scale={props.zScale} float={float}
gray={gray?.z} /> scale={props.zScale}
</Col> gray={gray?.z} />
</Col>
</Highlight>
</Row>; </Row>;
} }

View File

@ -56,8 +56,12 @@ const pinNumOrNamedPin =
} }
: pin; : pin;
const DISABLE_DDI = (): DropDownItem => ({
label: t("None"), value: 0
});
const listItems = (resources: ResourceIndex): DropDownItem[] => const listItems = (resources: ResourceIndex): DropDownItem[] =>
[...peripheralItems(resources), ...pinDropdowns(n => n)]; [DISABLE_DDI(), ...peripheralItems(resources), ...pinDropdowns(n => n)];
const peripheralItems = (resources: ResourceIndex): DropDownItem[] => { const peripheralItems = (resources: ResourceIndex): DropDownItem[] => {
const list = selectAllSavedPeripherals(resources) const list = selectAllSavedPeripherals(resources)

View File

@ -1,5 +1,11 @@
import { Dictionary } from "farmbot"; import { Dictionary } from "farmbot";
import { DiagnosticMessages } from "../../constants"; import { DiagnosticMessages } from "../../constants";
import { docLink } from "../../ui/doc_link";
import { trim } from "../../util/util";
const DiagnosticMessagesWiFiOrConfig =
trim(`${DiagnosticMessages.WIFI_OR_CONFIG}
${docLink("for-it-security-professionals")}`);
// I don't like this at all. // I don't like this at all.
// If anyone has a cleaner solution, I'd love to hear it. // If anyone has a cleaner solution, I'd love to hear it.
@ -16,13 +22,13 @@ export const TRUTH_TABLE: Readonly<Dictionary<string | undefined>> = {
// 17: No MQTT connections. // 17: No MQTT connections.
[0b10001]: DiagnosticMessages.NO_WS_AVAILABLE, [0b10001]: DiagnosticMessages.NO_WS_AVAILABLE,
// 24: Browser is connected to API and MQTT. // 24: Browser is connected to API and MQTT.
[0b11000]: DiagnosticMessages.WIFI_OR_CONFIG, [0b11000]: DiagnosticMessagesWiFiOrConfig,
// 9: At least the browser is connected to MQTT. // 9: At least the browser is connected to MQTT.
[0b01001]: DiagnosticMessages.WIFI_OR_CONFIG, [0b01001]: DiagnosticMessagesWiFiOrConfig,
// 8: At least the browser is connected to MQTT. // 8: At least the browser is connected to MQTT.
[0b01000]: DiagnosticMessages.WIFI_OR_CONFIG, [0b01000]: DiagnosticMessagesWiFiOrConfig,
// 25: Farmbot offline. // 25: Farmbot offline.
[0b11001]: DiagnosticMessages.WIFI_OR_CONFIG, [0b11001]: DiagnosticMessagesWiFiOrConfig,
// 2: Browser offline. Farmbot last seen by the API recently. // 2: Browser offline. Farmbot last seen by the API recently.
[0b00010]: DiagnosticMessages.NO_WS_AVAILABLE, [0b00010]: DiagnosticMessages.NO_WS_AVAILABLE,
// 18: Farmbot last seen by the API recently. // 18: Farmbot last seen by the API recently.

View File

@ -5,7 +5,6 @@ import { FarmbotOsSettings } from "./components/farmbot_os_settings";
import { Page, Col, Row } from "../ui/index"; import { Page, Col, Row } from "../ui/index";
import { mapStateToProps } from "./state_to_props"; import { mapStateToProps } from "./state_to_props";
import { Props } from "./interfaces"; import { Props } from "./interfaces";
import { PinBindings } from "./pin_bindings/pin_bindings";
import { getStatus } from "../connectivity/reducer_support"; import { getStatus } from "../connectivity/reducer_support";
import { isFwHardwareValue } from "./components/firmware_hardware_support"; import { isFwHardwareValue } from "./components/firmware_hardware_support";
@ -48,9 +47,6 @@ export class RawDevices extends React.Component<Props, {}> {
firmwareHardware={firmwareHardware} firmwareHardware={firmwareHardware}
sourceFwConfig={this.props.sourceFwConfig} sourceFwConfig={this.props.sourceFwConfig}
firmwareConfig={this.props.firmwareConfig} /> firmwareConfig={this.props.firmwareConfig} />
<PinBindings
dispatch={this.props.dispatch}
resources={this.props.resources} />
</Col> </Col>
</Row> </Row>
</Page>; </Page>;

View File

@ -93,7 +93,7 @@ export enum Feature {
variables = "variables", variables = "variables",
} }
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */ /** Object fetched from ExternalUrl.featureMinVersions. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>; export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
export interface BotState { export interface BotState {
@ -201,6 +201,7 @@ export interface PeripheralsProps {
peripherals: TaggedPeripheral[]; peripherals: TaggedPeripheral[];
dispatch: Function; dispatch: Function;
disabled: boolean | undefined; disabled: boolean | undefined;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface SensorsProps { export interface SensorsProps {
@ -208,6 +209,7 @@ export interface SensorsProps {
sensors: TaggedSensor[]; sensors: TaggedSensor[];
dispatch: Function; dispatch: Function;
disabled: boolean | undefined; disabled: boolean | undefined;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface FarmwareProps { export interface FarmwareProps {
@ -245,8 +247,11 @@ export interface HardwareSettingsProps {
export interface ControlPanelState { export interface ControlPanelState {
homing_and_calibration: boolean; homing_and_calibration: boolean;
motors: boolean; motors: boolean;
encoders_and_endstops: boolean; encoders: boolean;
danger_zone: boolean; endstops: boolean;
power_and_reset: boolean; error_handling: boolean;
pin_guard: boolean; pin_guard: boolean;
danger_zone: boolean;
pin_bindings: boolean;
power_and_reset: boolean;
} }

View File

@ -1,4 +1,5 @@
import { sortByNameAndPin, ButtonPin } from "../list_and_label_support"; import { sortByNameAndPin, ButtonPin, getSpecialActionLabel } from "../list_and_label_support";
import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
describe("sortByNameAndPin()", () => { describe("sortByNameAndPin()", () => {
@ -26,3 +27,11 @@ describe("sortByNameAndPin()", () => {
sortTest(1, 1, Order.equal); // GPIO 1 == GPIO 1 sortTest(1, 1, Order.equal); // GPIO 1 == GPIO 1
}); });
}); });
describe("getSpecialActionLabel()", () => {
it("handles undefined values", () => {
expect(getSpecialActionLabel(undefined)).toEqual("None");
expect(getSpecialActionLabel("wrong" as PinBindingSpecialAction))
.toEqual("");
});
});

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { PinBindings } from "../pin_bindings"; import { PinBindingsContent } from "../pin_bindings";
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 {
@ -8,15 +8,15 @@ import {
import { import {
fakeSequence, fakePinBinding fakeSequence, fakePinBinding
} from "../../../__test_support__/fake_state/resources"; } from "../../../__test_support__/fake_state/resources";
import { PinBindingsProps } from "../interfaces"; import { PinBindingsContentProps } from "../interfaces";
import { import {
SpecialPinBinding, SpecialPinBinding,
PinBindingType, PinBindingType,
PinBindingSpecialAction PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources"; } from "farmbot/dist/resources/api_resources";
describe("<PinBindings/>", () => { describe("<PinBindingsContent/>", () => {
function fakeProps(): PinBindingsProps { function fakeProps(): PinBindingsContentProps {
const fakeSequence1 = fakeSequence(); const fakeSequence1 = fakeSequence();
fakeSequence1.body.id = 1; fakeSequence1.body.id = 1;
fakeSequence1.body.name = "Sequence 1"; fakeSequence1.body.name = "Sequence 1";
@ -51,8 +51,8 @@ describe("<PinBindings/>", () => {
it("renders", () => { it("renders", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = mount(<PinBindings {...p} />); const wrapper = mount(<PinBindingsContent {...p} />);
["pin bindings", "pin number", "none", "bind", "stock bindings"] ["pin number", "none", "bind", "stock bindings"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string)); .map(string => expect(wrapper.text().toLowerCase()).toContain(string));
["26", "action"].map(string => ["26", "action"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string)); expect(wrapper.text().toLowerCase()).toContain(string));

View File

@ -4,7 +4,7 @@ import {
PinBindingSpecialAction PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources"; } from "farmbot/dist/resources/api_resources";
export interface PinBindingsProps { export interface PinBindingsContentProps {
dispatch: Function; dispatch: Function;
resources: ResourceIndex; resources: ResourceIndex;
} }

View File

@ -32,9 +32,14 @@ export const specialActionLabelLookup: { [x: string]: string } = {
export const specialActionList: DropDownItem[] = export const specialActionList: DropDownItem[] =
Object.values(PinBindingSpecialAction) Object.values(PinBindingSpecialAction)
.filter(action => action != PinBindingSpecialAction.dump_info)
.map((action: PinBindingSpecialAction) => .map((action: PinBindingSpecialAction) =>
({ label: specialActionLabelLookup[action], value: action })); ({ label: specialActionLabelLookup[action], value: action }));
export const getSpecialActionLabel =
(action: PinBindingSpecialAction | undefined) =>
specialActionLabelLookup[action || ""] || "";
/** Pin numbers for standard buttons. */ /** Pin numbers for standard buttons. */
export enum ButtonPin { export enum ButtonPin {
estop = 16, estop = 16,
@ -84,17 +89,17 @@ export const piSpi1Pins = [16, 17, 18, 19, 20, 21];
/** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */ /** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */
export const reservedPiGPIO = piI2c0Pins; export const reservedPiGPIO = piI2c0Pins;
const LabeledGpioPins: { [x: number]: string } = { const GPIO_PIN_LABELS = (): { [x: number]: string } => ({
[ButtonPin.estop]: "Button 1: E-STOP", [ButtonPin.estop]: t("Button {{ num }}: E-STOP", { num: 1 }),
[ButtonPin.unlock]: "Button 2: UNLOCK", [ButtonPin.unlock]: t("Button {{ num }}: UNLOCK", { num: 2 }),
[ButtonPin.btn3]: "Button 3", [ButtonPin.btn3]: t("Button {{ num }})", { num: 3 }),
[ButtonPin.btn4]: "Button 4", [ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }),
[ButtonPin.btn5]: "Button 5", [ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }),
}; });
export const generatePinLabel = (pin: number) => export const generatePinLabel = (pin: number) =>
LabeledGpioPins[pin] GPIO_PIN_LABELS()[pin]
? `${LabeledGpioPins[pin]} (Pi ${pin})` ? `${t(GPIO_PIN_LABELS()[pin])} (Pi ${pin})`
: `Pi GPIO ${pin}`; : `Pi GPIO ${pin}`;
/** Raspberry Pi GPIO pin numbers. */ /** Raspberry Pi GPIO pin numbers. */

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { Row, Col, FBSelect, NULL_CHOICE, DropDownItem } from "../../ui"; import { Row, Col, FBSelect, DropDownItem } from "../../ui";
import { PinBindingColWidth } from "./pin_bindings"; import { PinBindingColWidth } from "./pin_bindings";
import { Popover, Position } from "@blueprintjs/core"; import { Popover, Position } from "@blueprintjs/core";
import { RpiGpioDiagram } from "./rpi_gpio_diagram"; import { RpiGpioDiagram } from "./rpi_gpio_diagram";
@ -13,9 +13,10 @@ import { pinBindingBody } from "./tagged_pin_binding_init";
import { error, warning } from "../../toast/toast"; import { error, warning } from "../../toast/toast";
import { import {
validGpioPins, sysBindings, generatePinLabel, RpiPinList, validGpioPins, sysBindings, generatePinLabel, RpiPinList,
bindingTypeLabelLookup, specialActionLabelLookup, specialActionList, bindingTypeLabelLookup, specialActionList,
reservedPiGPIO, reservedPiGPIO,
bindingTypeList bindingTypeList,
getSpecialActionLabel
} from "./list_and_label_support"; } from "./list_and_label_support";
import { SequenceSelectBox } from "../../sequences/sequence_select_box"; import { SequenceSelectBox } from "../../sequences/sequence_select_box";
import { ResourceIndex } from "../../resources/interfaces"; import { ResourceIndex } from "../../resources/interfaces";
@ -119,8 +120,6 @@ export class PinBindingInputGroup
<BindingTypeDropDown <BindingTypeDropDown
bindingType={bindingType} bindingType={bindingType}
setBindingType={this.setBindingType} /> setBindingType={this.setBindingType} />
</Col>
<Col xs={PinBindingColWidth.target}>
{bindingType == PinBindingType.special {bindingType == PinBindingType.special
? <ActionTargetDropDown ? <ActionTargetDropDown
specialActionInput={specialActionInput} specialActionInput={specialActionInput}
@ -152,10 +151,10 @@ export const PinNumberInputGroup = (props: {
const selectedPinNumber = isNumber(pinNumberInput) ? { const selectedPinNumber = isNumber(pinNumberInput) ? {
label: generatePinLabel(pinNumberInput), label: generatePinLabel(pinNumberInput),
value: "" + pinNumberInput value: "" + pinNumberInput
} : NULL_CHOICE; } : undefined;
return <Row> return <Row>
<Col xs={1}> <Col xs={3}>
<Popover position={Position.TOP}> <Popover position={Position.TOP}>
<i className="fa fa-th-large" /> <i className="fa fa-th-large" />
<RpiGpioDiagram <RpiGpioDiagram
@ -181,7 +180,7 @@ export const BindingTypeDropDown = (props: {
setBindingType: (ddi: DropDownItem) => void, setBindingType: (ddi: DropDownItem) => void,
}) => { }) => {
const { bindingType, setBindingType } = props; const { bindingType, setBindingType } = props;
return <FBSelect return <FBSelect extraClass={"binding-type-dropdown"}
key={"binding_type_input_" + bindingType} key={"binding_type_input_" + bindingType}
onChange={setBindingType} onChange={setBindingType}
selectedItem={{ selectedItem={{
@ -213,12 +212,13 @@ export const ActionTargetDropDown = (props: {
const { specialActionInput, setSpecialAction } = props; const { specialActionInput, setSpecialAction } = props;
const selectedSpecialAction = specialActionInput ? { const selectedSpecialAction = specialActionInput ? {
label: specialActionLabelLookup[specialActionInput || ""], label: getSpecialActionLabel(specialActionInput),
value: "" + specialActionInput value: "" + specialActionInput
} : NULL_CHOICE; } : undefined;
return <FBSelect return <FBSelect
key={"special_action_input_" + specialActionInput} key={"special_action_input_" + specialActionInput}
customNullLabel={t("Select an action")}
onChange={setSpecialAction} onChange={setSpecialAction}
selectedItem={selectedSpecialAction} selectedItem={selectedSpecialAction}
list={specialActionList} />; list={specialActionList} />;

View File

@ -1,8 +1,8 @@
import * as React from "react"; import * as React from "react";
import { Widget, WidgetBody, WidgetHeader, Row, Col } from "../../ui"; import { Row, Col, Help } from "../../ui";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { selectAllPinBindings } from "../../resources/selectors"; import { selectAllPinBindings } from "../../resources/selectors";
import { PinBindingsProps, PinBindingListItems } from "./interfaces"; import { PinBindingsContentProps, PinBindingListItems } from "./interfaces";
import { PinBindingsList } from "./pin_bindings_list"; import { PinBindingsList } from "./pin_bindings_list";
import { PinBindingInputGroup } from "./pin_binding_input_group"; import { PinBindingInputGroup } from "./pin_binding_input_group";
import { import {
@ -20,9 +20,8 @@ import { t } from "../../i18next_wrapper";
/** Width of UI columns in Pin Bindings widget. */ /** Width of UI columns in Pin Bindings widget. */
export enum PinBindingColWidth { export enum PinBindingColWidth {
pin = 4, pin = 4,
type = 3, type = 6,
target = 4, button = 2
button = 1
} }
/** Use binding type to return a sequence ID or a special action. */ /** Use binding type to return a sequence ID or a special action. */
@ -64,34 +63,29 @@ const PinBindingsListHeader = () =>
<label> <label>
{t("Binding")} {t("Binding")}
</label> </label>
</Col> <Help text={ToolTips.PIN_BINDINGS} />
<Col xs={PinBindingColWidth.target}>
<label>
{t("target")}
</label>
</Col> </Col>
</Row>; </Row>;
export const PinBindings = (props: PinBindingsProps) => { export const PinBindingsContent = (props: PinBindingsContentProps) => {
const { dispatch, resources } = props; const { dispatch, resources } = props;
const pinBindings = apiPinBindings(resources); const pinBindings = apiPinBindings(resources);
return <Widget className="pin-bindings-widget"> return <div className="pin-bindings">
<WidgetHeader <Row>
title={t("Pin Bindings")} <StockPinBindingsButton dispatch={dispatch} />
helpText={ToolTips.PIN_BINDINGS}>
<Popover <Popover
position={Position.RIGHT_TOP} position={Position.RIGHT_TOP}
interactionKind={PopoverInteractionKind.HOVER} interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}> popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" /> <i className="fa fa-exclamation-triangle" />
<div> <div>
{t(ToolTips.PIN_BINDING_WARNING)} {t(ToolTips.PIN_BINDING_WARNING)}
</div> </div>
</Popover> </Popover>
<StockPinBindingsButton dispatch={dispatch} /> </Row>
</WidgetHeader> <div>
<WidgetBody>
<PinBindingsListHeader /> <PinBindingsListHeader />
<PinBindingsList <PinBindingsList
pinBindings={pinBindings} pinBindings={pinBindings}
@ -101,6 +95,6 @@ export const PinBindings = (props: PinBindingsProps) => {
pinBindings={pinBindings} pinBindings={pinBindings}
dispatch={dispatch} dispatch={dispatch}
resources={resources} /> resources={resources} />
</WidgetBody> </div>
</Widget>; </div>;
}; };

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { import {
bindingTypeLabelLookup, specialActionLabelLookup, bindingTypeLabelLookup,
generatePinLabel, sortByNameAndPin generatePinLabel, sortByNameAndPin, getSpecialActionLabel
} from "./list_and_label_support"; } from "./list_and_label_support";
import { destroy } from "../../api/crud"; import { destroy } from "../../api/crud";
import { error } from "../../toast/toast"; import { error } from "../../toast/toast";
@ -36,12 +36,10 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
{generatePinLabel(pin_number)} {generatePinLabel(pin_number)}
</Col> </Col>
<Col xs={PinBindingColWidth.type}> <Col xs={PinBindingColWidth.type}>
{t(bindingTypeLabelLookup[binding_type || ""])} {t(bindingTypeLabelLookup[binding_type || ""])}:&nbsp;
</Col>
<Col xs={PinBindingColWidth.target}>
{sequence_id {sequence_id
? findSequenceById(resources, sequence_id).body.name ? findSequenceById(resources, sequence_id).body.name
: t(specialActionLabelLookup[special_action || ""])} : t(getSpecialActionLabel(special_action))}
</Col> </Col>
<Col xs={PinBindingColWidth.button}> <Col xs={PinBindingColWidth.button}>
<button <button

View File

@ -11,7 +11,6 @@ import { maybeNegateStatus } from "../connectivity/maybe_negate_status";
import { ReduxAction } from "../redux/interfaces"; import { ReduxAction } from "../redux/interfaces";
import { connectivityReducer, PingResultPayload } from "../connectivity/reducer"; import { connectivityReducer, PingResultPayload } from "../connectivity/reducer";
import { versionOK } from "../util"; import { versionOK } from "../util";
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
import { DeepPartial } from "redux"; import { DeepPartial } from "redux";
import { incomingLegacyStatus } from "../connectivity/connect_device"; import { incomingLegacyStatus } from "../connectivity/connect_device";
import { merge } from "lodash"; import { merge } from "lodash";
@ -27,7 +26,10 @@ export const initialState = (): BotState => ({
controlPanelState: { controlPanelState: {
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
@ -116,7 +118,10 @@ export const botReducer = generateReducer<BotState>(initialState())
.add<boolean>(Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => { .add<boolean>(Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => {
s.controlPanelState.homing_and_calibration = a.payload; s.controlPanelState.homing_and_calibration = a.payload;
s.controlPanelState.motors = a.payload; s.controlPanelState.motors = a.payload;
s.controlPanelState.encoders_and_endstops = a.payload; s.controlPanelState.encoders = a.payload;
s.controlPanelState.endstops = a.payload;
s.controlPanelState.error_handling = a.payload;
s.controlPanelState.pin_bindings = a.payload;
s.controlPanelState.pin_guard = a.payload; s.controlPanelState.pin_guard = a.payload;
s.controlPanelState.danger_zone = a.payload; s.controlPanelState.danger_zone = a.payload;
return s; return s;
@ -199,8 +204,7 @@ function legacyStatusHandler(state: BotState,
const nextSyncStatus = maybeNegateStatus(info); const nextSyncStatus = maybeNegateStatus(info);
versionOK(informational_settings.controller_version, versionOK(informational_settings.controller_version);
EXPECTED_MAJOR, EXPECTED_MINOR);
state.hardware.informational_settings.sync_status = nextSyncStatus; state.hardware.informational_settings.sync_status = nextSyncStatus;
return state; return state;
} }

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