hardware settings ui updates

pull/1698/head
gabrielburnworth 2020-02-15 10:29:09 -08:00
parent 93d2521511
commit 9dab0c4bc5
43 changed files with 680 additions and 512 deletions

View File

@ -4,7 +4,10 @@ export const panelState = (): ControlPanelState => {
return {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false

View File

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

View File

@ -39,8 +39,8 @@ export namespace ToolTips {
few sequences to verify that everything works as expected.`);
export const PIN_BINDINGS =
trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is
activated.`);
trim(`Assign an action or sequence to execute when a Raspberry Pi
GPIO pin is activated.`);
export const PIN_BINDING_WARNING =
trim(`Warning: Binding to a pin without a physical button and
@ -51,24 +51,38 @@ export namespace ToolTips {
trim(`Diagnose connectivity issues with FarmBot and the browser.`);
// Hardware Settings: Homing and Calibration
export const HOMING =
export const HOMING_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
export const CALIBRATION =
export const HOMING_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, home axis
(find zero).`);
export const CALIBRATION_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis and determine
maximum.`);
export const CALIBRATION_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, home axis and
determine maximum.`);
export const SET_ZERO_POSITION =
trim(`Set the current location as zero.`);
export const FIND_HOME_ON_BOOT =
export const FIND_HOME_ON_BOOT_ENCODERS =
trim(`If encoders or end-stops are enabled, find the home position
when the device powers on.
Warning! This will perform homing on all axes when the
device powers on. Encoders or endstops must be enabled.
when the device powers on. Warning! This will perform homing on all
axes when the device powers on. Encoders or endstops must be enabled.
It is recommended to make sure homing works properly before enabling
this feature. (default: disabled)`);
export const FIND_HOME_ON_BOOT_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, find the home
position when the device powers on. Warning! This will perform homing
on all axes when the device powers on. Stall detection or endstops
must be enabled. It is recommended to make sure homing works properly
before enabling this feature. (default: disabled)`);
export const STOP_AT_HOME =
trim(`Stop at the home location of the axis. (default: disabled)`);
@ -85,18 +99,7 @@ export namespace ToolTips {
trim(`Set the length of each axis to provide software limits.
Used only if STOP AT MAX is enabled. (default: 0 (disabled))`);
export const TIMEOUT_AFTER =
trim(`Amount of time to wait for a command to execute before stopping.
(default: 120s)`);
// Hardware Settings: Motors
export const MAX_MOVEMENT_RETRIES =
trim(`Number of times to retry a movement before stopping. (default: 3)`);
export const E_STOP_ON_MOV_ERR =
trim(`Emergency stop if movement is not complete after the maximum
number of retries. (default: disabled)`);
export const MAX_SPEED =
trim(`Maximum travel speed after acceleration in millimeters per second.
(default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`);
@ -132,18 +135,22 @@ export namespace ToolTips {
export const MOTOR_CURRENT =
trim(`Motor current in milliamps. (default: 600)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENABLE_X2_MOTOR =
trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS.
(default: enabled)`);
// Hardware Settings: Encoders and Endstops
// Hardware Settings: Encoders / Stall Detection
export const ENABLE_ENCODERS =
trim(`Enable use of rotary encoders for stall detection,
calibration and homing. (default: enabled)`);
export const ENABLE_STALL_DETECTION =
trim(`Enable use of motor stall detection for detecting missed steps,
calibration and homing. (default: enabled)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENCODER_POSITIONING =
trim(`Use encoders for positioning. (default: disabled)`);
@ -151,17 +158,22 @@ export namespace ToolTips {
trim(`Reverse the direction of encoder position reading.
(default: disabled)`);
export const MAX_MISSED_STEPS =
export const MAX_MISSED_STEPS_ENCODERS =
trim(`Number of steps missed (determined by encoder) before motor is
considered to have stalled. (default: 5)`);
export const ENCODER_MISSED_STEP_DECAY =
export const MAX_MISSED_STEPS_STALL_DETECTION =
trim(`Number of steps missed (determined by motor stall detection) before
motor is considered to have stalled. (default: 5)`);
export const MISSED_STEP_DECAY =
trim(`Reduction to missed step total for every good step. (default: 5)`);
export const ENCODER_SCALING =
trim(`encoder scaling factor = 10000 * (motor resolution * microsteps)
/ (encoder resolution). (default: 5556 (10000*200/360))`);
// Hardware Settings: Endstops
export const ENABLE_ENDSTOPS =
trim(`Enable use of electronic end-stops for end detection,
calibration and homing. (default: disabled)`);
@ -173,6 +185,18 @@ export namespace ToolTips {
trim(`Invert axis end-stops. Enable for normally closed (NC),
disable for normally open (NO). (default: disabled)`);
// Hardware Settings: Error Handling
export const TIMEOUT_AFTER =
trim(`Amount of time to wait for a command to execute before stopping.
(default: 120s)`);
export const MAX_MOVEMENT_RETRIES =
trim(`Number of times to retry a movement before stopping. (default: 3)`);
export const E_STOP_ON_MOV_ERR =
trim(`Emergency stop if movement is not complete after the maximum
number of retries. (default: disabled)`);
// Hardware Settings: Pin Guard
export const PIN_GUARD_PIN_NUMBER =
trim(`The number of the pin to guard. This pin will be set to the specified
@ -263,8 +287,12 @@ export namespace ToolTips {
export const FIND_HOME =
trim(`The Find Home step instructs the device to perform a homing
command (using encoders or endstops) to find and set zero for
the chosen axis or axes.`);
command (using encoders, stall detection, or endstops) to find and set
zero for the chosen axis or axes.`);
export const CALIBRATE =
trim(`If encoders, stall detection, or end-stops are enabled,
home axis and determine maximum.`);
export const IF =
trim(`Execute a sequence if a condition is satisfied. If the condition
@ -715,8 +743,8 @@ export namespace Content {
export const END_DETECTION_DISABLED =
trim(`This command will not execute correctly because you do not have
encoders or endstops enabled for the chosen axis. Enable endstops or
encoders from the Device page for: `);
encoders, stall detection, or endstops enabled for the chosen axis.
Enable endstops, encoders, or stall detection from the Device page for: `);
export const IN_USE =
trim(`Used in another resource. Protected from deletion.`);

View File

@ -433,14 +433,17 @@ a {
}
}
.pin-bindings-widget {
.pin-bindings {
.fa-exclamation-triangle {
color: $orange;
margin-left: 1rem;
margin-top: 0.75rem;
}
.fa-th-large {
position: absolute;
top: 0.75rem;
left: 0.5rem;
color: $dark_gray;
margin-top: 0.5rem;
margin-left: 0.5rem;
}
.fb-button {
&.green {
@ -449,16 +452,27 @@ a {
}
.bindings-list {
margin-bottom: 1rem;
margin-left: 1rem;
font-size: 1.2rem;
}
.binding-type-dropdown {
margin-bottom: 1.5rem;
}
.stock-pin-bindings-button {
button {
margin: 0 !important;
margin: 1rem;
float: left;
margin-left: 2rem;
}
i {
margin-right: 0.5rem;
}
}
.bp3-popover-wrapper {
display: inline;
float: none !important;
margin-left: 1rem;
}
}
.sensor-history-widget {

View File

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

View File

@ -161,7 +161,6 @@ export class FarmbotOsSettings
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline} />
</MustBeOnline>
</WidgetBody>

View File

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

View File

@ -53,7 +53,6 @@ export interface PowerAndResetProps {
controlPanelState: ControlPanelState;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
botOnline: boolean;
}

View File

@ -4,23 +4,20 @@ import { Collapse, Popover, Position } from "@blueprintjs/core";
import { FactoryResetRow } from "./factory_reset_row";
import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form";
import { Feature } from "../../interfaces";
import { FbosButtonRow } from "./fbos_button_row";
import { Content } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper";
export function PowerAndReset(props: PowerAndResetProps) {
const { dispatch, sourceFbosConfig, shouldDisplay, botOnline } = props;
const { dispatch, sourceFbosConfig, botOnline } = props;
const { power_and_reset } = props.controlPanelState;
return <section>
<div style={{ fontSize: "1px" }}>
<Header
expanded={power_and_reset}
title={t("Power and Reset")}
name={"power_and_reset"}
dispatch={dispatch} />
</div>
<Header
expanded={power_and_reset}
title={t("Power and Reset")}
name={"power_and_reset"}
dispatch={dispatch} />
<Collapse isOpen={!!power_and_reset}>
<FbosButtonRow
botOnline={botOnline}
@ -36,14 +33,13 @@ export function PowerAndReset(props: PowerAndResetProps) {
buttonText={t("SHUTDOWN")}
color={"red"}
action={powerOff} />
{shouldDisplay(Feature.firmware_restart) &&
<FbosButtonRow
botOnline={botOnline}
label={t("RESTART FIRMWARE")}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />}
<FbosButtonRow
botOnline={botOnline}
label={t("RESTART FIRMWARE")}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />
<FactoryResetRow
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig}

View File

@ -6,7 +6,8 @@ import { MustBeOnline, isBotOnline } from "../must_be_online";
import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone";
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 { SpacePanelHeader } from "./hardware_settings/space_panel_header";
import {
@ -15,6 +16,8 @@ import {
import { Popover, Position } from "@blueprintjs/core";
import { FwParamExportMenu } from "./hardware_settings/export_menu";
import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling";
export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> {
@ -27,6 +30,7 @@ export class HardwareSettings extends
const { informational_settings } = this.props.bot.hardware;
const { sync_status } = informational_settings;
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
<MustBeOnline
@ -59,33 +63,30 @@ export class HardwareSettings extends
<div className="label-headings">
<SpacePanelHeader />
</div>
<HomingAndCalibration
dispatch={dispatch}
<HomingAndCalibration {...commonProps}
bot={bot}
sourceFwConfig={sourceFwConfig}
firmwareConfig={firmwareConfig}
firmwareHardware={firmwareHardware}
botDisconnected={botDisconnected} />
<Motors
dispatch={dispatch}
controlPanelState={controlPanelState}
<Motors {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<EncodersAndEndStops
dispatch={dispatch}
shouldDisplay={this.props.shouldDisplay}
controlPanelState={controlPanelState}
<Encoders {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<PinGuard
dispatch={dispatch}
resources={resources}
controlPanelState={controlPanelState}
<EndStops {...commonProps}
sourceFwConfig={sourceFwConfig} />
<DangerZone
dispatch={dispatch}
controlPanelState={controlPanelState}
<ErrorHandling {...commonProps}
sourceFwConfig={sourceFwConfig} />
<PinGuard {...commonProps}
resources={resources}
sourceFwConfig={sourceFwConfig} />
<DangerZone {...commonProps}
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
<PinBindings {...commonProps}
resources={resources} />
</MustBeOnline>
</WidgetBody>
</Widget>;

View File

@ -1,22 +1,40 @@
const mockDevice = {
calibrate: jest.fn(() => Promise.resolve({}))
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { mount } from "enzyme";
import { CalibrationRow } from "../calibration_row";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { CalibrationRowProps } from "../../interfaces";
describe("<CalibrationRow />", () => {
const fakeProps = (): CalibrationRowProps => ({
type: "calibrate",
hardware: bot.hardware.mcu_params,
botDisconnected: false,
action: jest.fn(),
toolTip: "calibrate",
title: "calibrate",
axisTitle: "calibrate",
});
describe("<HomingRow />", () => {
it("calls device", () => {
const result = mount(<CalibrationRow
hardware={bot.hardware.mcu_params}
botDisconnected={false} />);
const p = fakeProps();
const result = mount(<CalibrationRow {...p} />);
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"));
expect(mockDevice.calibrate).toHaveBeenCalledTimes(2);
[{ axis: "y" }, { axis: "x" }].map(x =>
expect(mockDevice.calibrate).toHaveBeenCalledWith(x));
expect(p.action).toHaveBeenCalledTimes(2);
["y", "x"].map(x => expect(p.action).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,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 { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { HomingAndCalibration } from "../homing_and_calibration";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { updateMCU } from "../../../actions";
@ -10,20 +20,28 @@ import {
} from "../../../../__test_support__/fake_state/resources";
import { error, warning } from "../../../../toast/toast";
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 />", () => {
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(
provided: string, expected: string | undefined) {
const dispatch = jest.fn();
bot.controlPanelState.homing_and_calibration = true;
const result = mount(<HomingAndCalibration
dispatch={dispatch}
bot={bot}
firmwareConfig={fakeFirmwareConfig().body}
sourceFwConfig={x => ({
value: bot.hardware.mcu_params[x], consistent: true
})}
botDisconnected={false} />);
const p = fakeProps();
p.bot.controlPanelState.homing_and_calibration = true;
const result = mount(<HomingAndCalibration {...p} />);
const e = inputEvent(provided);
const input = result.find("input").first().props();
input.onChange && input.onChange(e);
@ -45,4 +63,33 @@ describe("<HomingAndCalibration />", () => {
expect(warning).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", () => {
const wrapper = render(<Motors {...fakeProps()} />);
["Enable 2nd X Motor",
"Max Retries",
"E-Stop on Movement Error",
"Max Speed (mm/s)"
].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
@ -48,16 +46,14 @@ describe("<Motors/>", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).toContain("Stall");
expect(wrapper.text()).toContain("Current");
expect(wrapper.text()).toContain("Motor Current");
});
it("doesn't show TMC parameters", () => {
const p = fakeProps();
p.firmwareHardware = "farmduino";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).not.toContain("Stall");
expect(wrapper.text()).not.toContain("Current");
expect(wrapper.text()).not.toContain("Motor Current");
});
const testParamToggle = (
@ -72,15 +68,6 @@ describe("<Motors/>", () => {
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", 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");
});
testParamToggle("toggles enable X2", "movement_secondary_motor_x", 6);
testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 7);
});

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

View File

@ -5,41 +5,53 @@ 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) {
export function Encoders(props: EncodersProps) {
const { encoders_and_endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props;
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 <section>
<Header
expanded={encoders_and_endstops}
title={isExpressBoard(firmwareHardware)
? t("Stall Detection and Endstops")
: t("Encoders and Endstops")}
name={"encoders_and_endstops"}
expanded={encoders}
title={isExpress
? t("Stall Detection")
: t("Encoders")}
name={"encoders"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders_and_endstops}>
<Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup
name={isExpressBoard(firmwareHardware)
name={isExpress
? t("Enable Stall Detection")
: t("Enable Encoders")}
tooltip={ToolTips.ENABLE_ENCODERS}
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} />
{!isExpressBoard(firmwareHardware) &&
{isExpress &&
<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} />}
{!isExpress &&
<BooleanMCUInputGroup
name={t("Use Encoders for Positioning")}
tooltip={ToolTips.ENCODER_POSITIONING}
@ -49,7 +61,7 @@ export function EncodersAndEndStops(props: EncodersProps) {
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpressBoard(firmwareHardware) &&
{!isExpress &&
<BooleanMCUInputGroup
name={t("Invert Encoders")}
tooltip={ToolTips.INVERT_ENCODERS}
@ -61,7 +73,9 @@ export function EncodersAndEndStops(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
name={t("Max Missed Steps")}
tooltip={ToolTips.MAX_MISSED_STEPS}
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"}
@ -70,14 +84,14 @@ export function EncodersAndEndStops(props: EncodersProps) {
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Missed Step Decay")}
tooltip={ToolTips.ENCODER_MISSED_STEP_DECAY}
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} />
{!isExpressBoard(firmwareHardware) &&
{!isExpress &&
<NumericMCUInputGroup
name={t("Encoder Scaling")}
tooltip={ToolTips.ENCODER_SCALING}
@ -87,44 +101,10 @@ export function EncodersAndEndStops(props: EncodersProps) {
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"}
intSize={"long"}
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,57 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { EndStopsProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
export function EndStops(props: EndStopsProps) {
const { endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
return <section>
<Header
expanded={endstops}
title={"Endstops"}
name={"endstops"}
dispatch={dispatch} />
<Collapse isOpen={!!endstops}>
<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,53 @@
import * as React from "react";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { ToolTips } from "../../../constants";
import { ErrorHandlingProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { McuInputBox } from "../mcu_input_box";
import { settingToggle } from "../../actions";
import { SingleSettingRow } from "./single_setting_row";
import { ToggleButton } from "../../../controls/toggle_button";
export function ErrorHandling(props: ErrorHandlingProps) {
const { error_handling } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
return <section>
<Header
expanded={error_handling}
title={"Error Handling"}
name={"error_handling"}
dispatch={dispatch} />
<Collapse isOpen={!!error_handling}>
<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} />
<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>
</Collapse>
</section>;
}

View File

@ -2,19 +2,23 @@ import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { HomingRow } from "./homing_row";
import { CalibrationRow } from "./calibration_row";
import { ZeroRow } from "./zero_row";
import { disabledAxisMap } from "../axis_tracking_status";
import { HomingAndCalibrationProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
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";
export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const { dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected
const {
dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected,
firmwareHardware
} = props;
const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params;
const { homing_and_calibration } = props.bot.controlPanelState;
@ -34,12 +38,43 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch}
expanded={homing_and_calibration} />
<Collapse isOpen={!!homing_and_calibration}>
<HomingRow hardware={hardware} botDisconnected={botDisconnected} />
<CalibrationRow hardware={hardware} botDisconnected={botDisconnected} />
<ZeroRow botDisconnected={botDisconnected} />
<CalibrationRow
type={"find_home"}
title={t("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={t("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={t("SET ZERO POSITION")}
axisTitle={t("ZERO")}
toolTip={ToolTips.SET_ZERO_POSITION}
action={axis => getDevice().setZero(axis)
.catch(commandErr("Zeroing"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<BooleanMCUInputGroup
name={t("Find Home on Boot")}
tooltip={ToolTips.FIND_HOME_ON_BOOT}
tooltip={isExpressBoard(firmwareHardware)
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
disable={disabled}
x={"movement_home_at_boot_x"}
y={"movement_home_at_boot_y"}
@ -88,14 +123,6 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
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>
</section>;
}

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

@ -5,32 +5,14 @@ import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { MotorsProps } from "../interfaces";
import { Row, Col, Help } from "../../../ui/index";
import { Header } from "./header";
import { Collapse, Position } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { Xyz, McuParamName } from "farmbot";
import { SourceFwConfig } from "../../interfaces";
import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props";
import { isTMCBoard, isExpressBoard } from "../firmware_hardware_support";
const SingleSettingRow =
({ 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>;
import { isTMCBoard } from "../firmware_hardware_support";
import { SingleSettingRow } from "./single_setting_row";
export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
@ -51,13 +33,8 @@ export function Motors(props: MotorsProps) {
} = props;
const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x");
const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x");
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
const scale = calculateScale(sourceFwConfig);
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value,
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value,
};
return <section>
<Header
expanded={controlPanelState.motors}
@ -65,23 +42,6 @@ export function Motors(props: MotorsProps) {
name={"motors"}
dispatch={dispatch} />
<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
name={t("Max Speed (mm/s)")}
tooltip={ToolTips.MAX_SPEED}
@ -171,16 +131,6 @@ export function Motors(props: MotorsProps) {
z={"movement_motor_current_z"}
dispatch={dispatch}
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"
label={t("Enable 2nd X Motor")}
tooltip={ToolTips.ENABLE_X2_MOTOR}>

View File

@ -0,0 +1,22 @@
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";
export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props;
return <section>
<Header
expanded={pin_bindings}
title={"Pin Bindings"}
name={"pin_bindings"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} />
</Collapse>
</section>;
}

View File

@ -0,0 +1,20 @@
import * as React from "react";
import { Row, Col, Help } from "../../../ui/index";
import { Position } from "@blueprintjs/core";
export const SingleSettingRow =
({ 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>;

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,17 +1,12 @@
import {
BotState, Xyz, SourceFwConfig,
ControlPanelState, ShouldDisplay
ControlPanelState, Axis
} from "../interfaces";
import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist";
import { IntegerSize } from "../../util";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { ResourceIndex } from "../../resources/interfaces";
export interface HomingRowProps {
hardware: McuParams;
botDisconnected: boolean;
}
export interface ZeroRowProps {
botDisconnected: boolean;
}
@ -19,9 +14,11 @@ export interface ZeroRowProps {
export interface HomingAndCalibrationProps {
dispatch: Function;
bot: BotState;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
firmwareConfig: FirmwareConfig | undefined;
botDisconnected: boolean;
firmwareHardware: FirmwareHardware | undefined;
}
export interface BooleanMCUInputGroupProps {
@ -39,8 +36,13 @@ export interface BooleanMCUInputGroupProps {
}
export interface CalibrationRowProps {
type: "find_home" | "calibrate" | "zero";
hardware: McuParams;
botDisconnected: boolean;
action(axis: Axis): void;
toolTip: string;
title: string;
axisTitle: string;
}
export interface NumericMCUInputGroupProps {
@ -85,12 +87,29 @@ export interface MotorsProps {
export interface EncodersProps {
dispatch: Function;
shouldDisplay: ShouldDisplay;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
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 {
dispatch: Function;
controlPanelState: ControlPanelState;

View File

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

View File

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

View File

@ -245,7 +245,10 @@ export interface HardwareSettingsProps {
export interface ControlPanelState {
homing_and_calibration: boolean;
motors: boolean;
encoders_and_endstops: boolean;
encoders: boolean;
endstops: boolean;
error_handling: boolean;
pin_bindings: boolean;
danger_zone: boolean;
power_and_reset: boolean;
pin_guard: 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()", () => {
@ -26,3 +27,11 @@ describe("sortByNameAndPin()", () => {
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 { PinBindings } from "../pin_bindings";
import { PinBindingsContent } from "../pin_bindings";
import { mount } from "enzyme";
import { bot } from "../../../__test_support__/fake_state/bot";
import {
@ -8,15 +8,15 @@ import {
import {
fakeSequence, fakePinBinding
} from "../../../__test_support__/fake_state/resources";
import { PinBindingsProps } from "../interfaces";
import { PinBindingsContentProps } from "../interfaces";
import {
SpecialPinBinding,
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
describe("<PinBindings/>", () => {
function fakeProps(): PinBindingsProps {
describe("<PinBindingsContent/>", () => {
function fakeProps(): PinBindingsContentProps {
const fakeSequence1 = fakeSequence();
fakeSequence1.body.id = 1;
fakeSequence1.body.name = "Sequence 1";
@ -51,8 +51,8 @@ describe("<PinBindings/>", () => {
it("renders", () => {
const p = fakeProps();
const wrapper = mount(<PinBindings {...p} />);
["pin bindings", "pin number", "none", "bind", "stock bindings"]
const wrapper = mount(<PinBindingsContent {...p} />);
["pin number", "none", "bind", "stock bindings"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
["26", "action"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));

View File

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

View File

@ -32,9 +32,14 @@ export const specialActionLabelLookup: { [x: string]: string } = {
export const specialActionList: DropDownItem[] =
Object.values(PinBindingSpecialAction)
.filter(action => action != PinBindingSpecialAction.dump_info)
.map((action: PinBindingSpecialAction) =>
({ label: specialActionLabelLookup[action], value: action }));
export const getSpecialActionLabel =
(action: PinBindingSpecialAction | undefined) =>
specialActionLabelLookup[action || ""] || "";
/** Pin numbers for standard buttons. */
export enum ButtonPin {
estop = 16,

View File

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

View File

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

View File

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

View File

@ -27,7 +27,10 @@ export const initialState = (): BotState => ({
controlPanelState: {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false
@ -116,7 +119,10 @@ export const botReducer = generateReducer<BotState>(initialState())
.add<boolean>(Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => {
s.controlPanelState.homing_and_calibration = 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.danger_zone = a.payload;
return s;

View File

@ -69,7 +69,7 @@ class InnerTileCalibrate extends React.Component<CalibrateParams, {}> {
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.CALIBRATION}
helpText={ToolTips.CALIBRATE}
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}