sequence step warnings

pull/643/head
gabrielburnworth 2018-01-24 16:33:16 -08:00
parent f3caa88f73
commit 465fa266eb
18 changed files with 304 additions and 42 deletions

View File

@ -0,0 +1,11 @@
import { HardwareFlags } from "../sequences/interfaces";
export const fakeHardwareFlags = (): HardwareFlags => {
return {
findHomeEnabled: { x: false, y: false, z: false },
stopAtHome: { x: false, y: false, z: false },
stopAtMax: { x: false, y: false, z: false },
negativeOnly: { x: false, y: false, z: false },
axisLength: { x: 0, y: 0, z: 0 },
};
};

View File

@ -409,6 +409,11 @@ export namespace Content {
trim(`No Sequence selected. Click one in the Sequences panel to edit,
or click "+" to create a new one.`);
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: `);
// Regimens
export const NO_REGIMEN_SELECTED =
trim(`No Regimen selected. Click one in the Regimens panel to edit, or

View File

@ -36,7 +36,6 @@
border-top-left-radius: 3px;
border-top-right-radius: 3px;
.pt-popover-target {
display: inline-block;
float: right;
margin-top: 0.1rem;
}
@ -103,6 +102,13 @@
.help {
float: right;
}
.step-warning {
display: inline-block;
float: none;
.pt-popover-target {
float: left;
}
}
}
.step-control {

View File

@ -25,6 +25,8 @@ import {
} from "../../__test_support__/resource_index_builder";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { destroy } from "../../api/crud";
import { fakeHardwareFlags } from "../../__test_support__/sequence_hardware_settings";
describe("<SequenceEditorMiddleActive/>", () => {
function fakeProps(): ActiveMiddleProps {
@ -34,7 +36,8 @@ describe("<SequenceEditorMiddleActive/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced",
consistent: true,
autoSyncEnabled: false
autoSyncEnabled: false,
hardwareFlags: fakeHardwareFlags()
};
}

View File

@ -6,6 +6,7 @@ import {
FAKE_RESOURCES, buildResourceIndex
} from "../../__test_support__/resource_index_builder";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { fakeHardwareFlags } from "../../__test_support__/sequence_hardware_settings";
describe("<SequenceEditorMiddle/>", () => {
function fakeProps(): SequenceEditorMiddleProps {
@ -15,7 +16,8 @@ describe("<SequenceEditorMiddle/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced",
consistent: true,
autoSyncEnabled: false
autoSyncEnabled: false,
hardwareFlags: fakeHardwareFlags()
};
}

View File

@ -12,6 +12,7 @@ import {
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { auth } from "../../__test_support__/fake_state/token";
import { ToolTips } from "../../constants";
import { fakeHardwareFlags } from "../../__test_support__/sequence_hardware_settings";
describe("<Sequences/>", () => {
function fakeProps(): Props {
@ -23,7 +24,8 @@ describe("<Sequences/>", () => {
syncStatus: "synced",
auth,
consistent: true,
autoSyncEnabled: false
autoSyncEnabled: false,
hardwareFlags: fakeHardwareFlags()
};
}

View File

@ -6,17 +6,19 @@ import { StepDragger } from "../draggable/step_dragger";
import { renderCeleryNode } from "./step_tiles/index";
import { ResourceIndex } from "../resources/interfaces";
import { getStepTag } from "../resources/sequence_tagging";
import { HardwareFlags } from "./interfaces";
interface AllStepsProps {
sequence: TaggedSequence;
onDrop(index: number, key: string): void;
dispatch: Function;
resources: ResourceIndex;
hardwareFlags?: HardwareFlags;
}
export class AllSteps extends React.Component<AllStepsProps, {}> {
render() {
const { sequence, onDrop, dispatch } = this.props;
const { sequence, onDrop, dispatch, hardwareFlags } = this.props;
const items = (sequence.body.body || [])
.map((currentStep: SequenceBodyItem, index, arr) => {
/** HACK: React's diff algorithm (probably?) can't keep track of steps
@ -39,7 +41,8 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
index,
dispatch: dispatch,
currentSequence: sequence,
resources: this.props.resources
resources: this.props.resources,
hardwareFlags
})}
</div>
</StepDragger>

View File

@ -5,13 +5,22 @@ import {
SequenceBodyItem,
LegalArgString,
SyncStatus,
ALLOWED_CHANNEL_NAMES
ALLOWED_CHANNEL_NAMES,
Xyz
} from "farmbot";
import { StepMoveDataXfer, StepSpliceDataXfer } from "../draggable/interfaces";
import { TaggedSequence } from "../resources/tagged_resources";
import { ResourceIndex } from "../resources/interfaces";
import { JSXChildren } from "../util";
export interface HardwareFlags {
findHomeEnabled: Record<Xyz, boolean>;
stopAtHome: Record<Xyz, boolean>;
stopAtMax: Record<Xyz, boolean>;
negativeOnly: Record<Xyz, boolean>;
axisLength: Record<Xyz, number>;
}
export interface Props {
dispatch: Function;
sequences: TaggedSequence[];
@ -21,6 +30,7 @@ export interface Props {
syncStatus: SyncStatus;
consistent: boolean;
autoSyncEnabled: boolean;
hardwareFlags: HardwareFlags;
}
export interface SequenceEditorMiddleProps {
@ -30,6 +40,7 @@ export interface SequenceEditorMiddleProps {
syncStatus: SyncStatus;
consistent: boolean;
autoSyncEnabled: boolean;
hardwareFlags: HardwareFlags;
}
export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
@ -146,4 +157,5 @@ export interface StepParams {
dispatch: Function;
index: number;
resources: ResourceIndex;
hardwareFlags?: HardwareFlags;
}

View File

@ -11,7 +11,8 @@ export class SequenceEditorMiddle
dispatch,
sequence,
resources,
syncStatus
syncStatus,
hardwareFlags
} = this.props;
if (sequence && isTaggedSequence(sequence)) {
return <SequenceEditorMiddleActive
@ -20,7 +21,8 @@ export class SequenceEditorMiddle
resources={resources}
syncStatus={syncStatus}
consistent={true}
autoSyncEnabled={false} />;
autoSyncEnabled={false}
hardwareFlags={hardwareFlags} />;
} else {
return <SequenceEditorMiddleInactive />;
}

View File

@ -39,7 +39,8 @@ export class Sequences extends React.Component<Props, {}> {
sequence={this.props.sequence}
resources={this.props.resources}
consistent={this.props.consistent}
autoSyncEnabled={this.props.autoSyncEnabled} />
autoSyncEnabled={this.props.autoSyncEnabled}
hardwareFlags={this.props.hardwareFlags} />
</div>
</Col>
<Col sm={3}>

View File

@ -1,16 +1,47 @@
import { Everything } from "../interfaces";
import { Props } from "./interfaces";
import { Props, HardwareFlags } from "./interfaces";
import {
selectAllSequences,
findSequence
} from "../resources/selectors";
import { getStepTag } from "../resources/sequence_tagging";
import { enabledAxisMap } from "../devices/components/axis_tracking_status";
export function mapStateToProps(props: Everything): Props {
const uuid = props.resources.consumers.sequences.current;
const sequence = uuid ? findSequence(props.resources.index, uuid) : undefined;
sequence && (sequence.body.body || []).map(x => getStepTag(x));
const hardwareFlags = (): HardwareFlags => {
const { mcu_params } = props.bot.hardware;
return {
findHomeEnabled: enabledAxisMap(mcu_params),
stopAtHome: {
x: !!mcu_params.movement_stop_at_home_x,
y: !!mcu_params.movement_stop_at_home_y,
z: !!mcu_params.movement_stop_at_home_z
},
stopAtMax: {
x: !!mcu_params.movement_stop_at_max_x,
y: !!mcu_params.movement_stop_at_max_y,
z: !!mcu_params.movement_stop_at_max_z
},
negativeOnly: {
x: !!mcu_params.movement_home_up_x,
y: !!mcu_params.movement_home_up_y,
z: !!mcu_params.movement_home_up_z
},
axisLength: {
x: (mcu_params.movement_axis_nr_steps_x || 0)
/ (mcu_params.movement_step_per_mm_x || 1),
y: (mcu_params.movement_axis_nr_steps_y || 0)
/ (mcu_params.movement_step_per_mm_y || 1),
z: (mcu_params.movement_axis_nr_steps_z || 0)
/ (mcu_params.movement_step_per_mm_z || 1)
},
};
};
return {
dispatch: props.dispatch,
sequences: selectAllSequences(props.resources.index),
@ -23,6 +54,7 @@ export function mapStateToProps(props: Everything): Props {
.informational_settings
.sync_status || "unknown"),
consistent: props.bot.consistent,
autoSyncEnabled: !!props.bot.hardware.configuration.auto_sync
autoSyncEnabled: !!props.bot.hardware.configuration.auto_sync,
hardwareFlags: hardwareFlags(),
};
}

View File

@ -4,9 +4,12 @@ import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { FindHome } from "farmbot/dist";
import { emptyState } from "../../../resources/reducer";
import {
fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings";
describe("<TileFindHome/>", () => {
function bootstrapTest() {
const fakeProps = () => {
const currentStep: FindHome = {
kind: "find_home",
args: {
@ -15,19 +18,19 @@ describe("<TileFindHome/>", () => {
}
};
return {
component: mount(<TileFindHome
currentSequence={fakeSequence()}
currentStep={currentStep}
dispatch={jest.fn()}
index={0}
resources={emptyState().index} />)
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
hardwareFlags: fakeHardwareFlags()
};
}
};
it("renders inputs", () => {
const block = bootstrapTest().component;
const inputs = block.find("input");
const labels = block.find("label");
const wrapper = mount(<TileFindHome {...fakeProps() } />);
const inputs = wrapper.find("input");
const labels = wrapper.find("label");
expect(inputs.length).toEqual(5);
expect(labels.length).toEqual(4);
expect(inputs.first().props().placeholder).toEqual("Find Home");
@ -40,4 +43,28 @@ describe("<TileFindHome/>", () => {
expect(labels.at(3).text()).toContain("Find all");
expect(inputs.at(4).props().checked).toBeTruthy();
});
it("doesn't render warning", () => {
const p = fakeProps();
p.currentStep.args.axis = "x";
p.hardwareFlags.findHomeEnabled.x = true;
const wrapper = mount(<TileFindHome {...p} />);
expect(wrapper.text()).not.toContain("Hardware setting conflict.");
});
it("renders warning: all axes", () => {
const p = fakeProps();
p.currentStep.args.axis = "all";
p.hardwareFlags.findHomeEnabled.x = false;
const wrapper = mount(<TileFindHome {...p} />);
expect(wrapper.text()).toContain("Hardware setting conflict.");
});
it("renders warning: one axis", () => {
const p = fakeProps();
p.currentStep.args.axis = "x";
p.hardwareFlags.findHomeEnabled.x = false;
const wrapper = mount(<TileFindHome {...p} />);
expect(wrapper.text()).toContain("Hardware setting conflict.");
});
});

View File

@ -6,9 +6,10 @@ import { MoveAbsolute, SequenceBodyItem } from "farmbot/dist";
import { emptyState } from "../../../resources/reducer";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { SpecialStatus } from "../../../resources/tagged_resources";
import { fakeHardwareFlags } from "../../../__test_support__/sequence_hardware_settings";
describe("<TileMoveAbsolute/>", () => {
function bootstrapTest() {
const fakeProps = () => {
const currentStep: MoveAbsolute = {
kind: "move_absolute",
args: {
@ -32,14 +33,14 @@ describe("<TileMoveAbsolute/>", () => {
}
};
return {
component: mount(<TileMoveAbsolute
currentSequence={fakeSequence()}
currentStep={currentStep}
dispatch={jest.fn()}
index={0}
resources={emptyState().index} />)
currentSequence: fakeSequence(),
currentStep: currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
hardwareFlags: fakeHardwareFlags()
};
}
};
function checkField(
block: ReactWrapper, position: number, label: string, value: string | number
@ -51,7 +52,7 @@ describe("<TileMoveAbsolute/>", () => {
}
it("renders inputs", () => {
const block = bootstrapTest().component;
const block = mount(<TileMoveAbsolute {...fakeProps() } />);
const inputs = block.find("input");
const labels = block.find("label");
const buttons = block.find("button");
@ -103,4 +104,58 @@ describe("<TileMoveAbsolute/>", () => {
expect(component.tool).toEqual(tool);
});
const conflictText = "Hardware setting conflict.";
it("doesn't show setting warning", () => {
const p = fakeProps();
const wrapper = mount(<TileMoveAbsolute {...p} />);
expect(wrapper.text()).not.toContain(conflictText);
});
it("doesn't show warning: axis length 0", () => {
const p = fakeProps();
p.currentStep.args.offset.args.x = 10000;
p.hardwareFlags.stopAtMax.x = true;
p.hardwareFlags.axisLength.x = 0;
const wrapper = mount(<TileMoveAbsolute {...p} />);
expect(wrapper.text()).not.toContain(conflictText);
});
it("shows warning: too high", () => {
const p = fakeProps();
p.currentStep.args.offset.args.x = 10000;
p.hardwareFlags.stopAtMax.x = true;
p.hardwareFlags.axisLength.x = 100;
const wrapper = mount(<TileMoveAbsolute {...p} />);
expect(wrapper.text()).toContain(conflictText);
});
it("shows warning: too high (negativeOnly)", () => {
const p = fakeProps();
p.currentStep.args.offset.args.x = -10000;
p.hardwareFlags.stopAtMax.x = true;
p.hardwareFlags.negativeOnly.x = true;
p.hardwareFlags.axisLength.x = 100;
const wrapper = mount(<TileMoveAbsolute {...p} />);
expect(wrapper.text()).toContain(conflictText);
});
it("shows warning: too low (negativeOnly)", () => {
const p = fakeProps();
p.currentStep.args.offset.args.x = 10000;
p.hardwareFlags.stopAtHome.x = true;
p.hardwareFlags.negativeOnly.x = true;
const wrapper = mount(<TileMoveAbsolute {...p} />);
expect(wrapper.text()).toContain(conflictText);
});
it("shows warning: too low", () => {
const p = fakeProps();
p.currentStep.args.offset.args.x = -10000;
p.hardwareFlags.stopAtHome.x = true;
p.hardwareFlags.stopAtMax.x = true;
const wrapper = mount(<TileMoveAbsolute {...p} />);
expect(wrapper.text()).toContain(conflictText);
});
});

View File

@ -1,13 +1,16 @@
import * as React from "react";
import { FindHome, ALLOWED_AXIS } from "farmbot";
import { StepParams } from "../interfaces";
import { FindHome, ALLOWED_AXIS, Xyz } from "farmbot";
import { StepParams, HardwareFlags } from "../interfaces";
import { TaggedSequence } from "../../resources/tagged_resources";
import { ResourceIndex } from "../../resources/interfaces";
import { overwrite } from "../../api/crud";
import { defensiveClone } from "../../util";
import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { ToolTips, Content } from "../../constants";
import {
StepWrapper, StepHeader, StepContent, StepWarning
} from "../step_ui/index";
import { Row, Col } from "../../ui/index";
import { some } from "lodash";
export function TileFindHome(props: StepParams) {
if (props.currentStep.kind === "find_home") {
@ -16,9 +19,10 @@ export function TileFindHome(props: StepParams) {
currentSequence={props.currentSequence}
dispatch={props.dispatch}
index={props.index}
resources={props.resources} />;
resources={props.resources}
hardwareFlags={props.hardwareFlags} />;
} else {
throw new Error("TileFindHome expects send_message");
throw new Error("TileFindHome expects find_home");
}
}
interface FindHomeParams {
@ -27,6 +31,7 @@ interface FindHomeParams {
dispatch: Function;
index: number;
resources: ResourceIndex;
hardwareFlags: HardwareFlags | undefined;
}
const AXIS_CHOICES: ALLOWED_AXIS[] = ["x", "y", "z", "all"];
@ -45,6 +50,36 @@ class InnerFindHome extends React.Component<FindHomeParams, {}> {
this.props.dispatch(overwrite(this.props.currentSequence, nextSequence));
}
get settingConflicts(): Record<Xyz, boolean> {
const conflicts = { x: false, y: false, z: false };
if (this.props.hardwareFlags) {
const { axis } = this.props.currentStep.args;
const { findHomeEnabled } = this.props.hardwareFlags;
switch (axis) {
case "x":
case "y":
case "z":
conflicts[axis] = !findHomeEnabled[axis];
break;
case "all":
conflicts.x = !findHomeEnabled.x;
conflicts.y = !findHomeEnabled.y;
conflicts.z = !findHomeEnabled.z;
break;
}
}
return conflicts;
}
get settingConflictWarning() {
const conflictAxes: string[] = [];
Object.entries(this.settingConflicts)
.map(([label, value]) => {
if (value) { conflictAxes.push(label); }
});
return Content.END_DETECTION_DISABLED + conflictAxes.join(", ");
}
render() {
const { dispatch, index, currentStep, currentSequence } = this.props;
@ -56,7 +91,10 @@ class InnerFindHome extends React.Component<FindHomeParams, {}> {
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
index={index} />
index={index}>
{some(this.settingConflicts) &&
StepWarning(this.settingConflictWarning)}
</StepHeader>
<StepContent className={className}>
<Row>
<Col xs={12}>

View File

@ -29,7 +29,7 @@ import { Xyz } from "../../devices/interfaces";
import { TileMoveAbsSelect, InputBox } from "./tile_move_absolute/index";
import { ToolTips } from "../../constants";
import { extractParent } from "../locals_list";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { StepWrapper, StepHeader, StepContent, StepWarning } from "../step_ui/index";
import { StepInputBox } from "../inputs/step_input_box";
interface Args {
@ -118,6 +118,38 @@ export class TileMoveAbsolute extends Component<StepParams, MoveAbsState> {
this.updateArgs(_.merge({}, this.args, update));
}
get settingConflicts(): Record<Xyz, boolean> {
const conflicts = { x: false, y: false, z: false };
if (this.props.hardwareFlags) {
const {
stopAtHome, stopAtMax, negativeOnly, axisLength
} = this.props.hardwareFlags;
["x", "y", "z"].map((axis: Xyz) => {
const coord = parseFloat(this.getAxisValue(axis));
const offset = parseFloat(this.getOffsetValue(axis));
const sum = coord + offset;
if (stopAtHome[axis]) {
conflicts[axis] = negativeOnly[axis] ? sum > 0 : sum < 0;
}
if (stopAtMax[axis] && axisLength[axis] !== 0) {
conflicts[axis] = conflicts[axis] || (negativeOnly[axis]
? sum < -axisLength[axis]
: sum > axisLength[axis]);
}
});
}
return conflicts;
}
get settingConflictWarning() {
const conflictAxes: string[] = [];
Object.entries(this.settingConflicts)
.map(([label, value]) => {
if (value) { conflictAxes.push(label); }
});
return "Movement out of bounds for: " + conflictAxes.join(", ");
}
render() {
const { currentStep, dispatch, index, currentSequence } = this.props;
if (currentSequence && !isTaggedSequence(currentSequence)) {
@ -132,7 +164,10 @@ export class TileMoveAbsolute extends Component<StepParams, MoveAbsState> {
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
index={index} />
index={index}>
{_.some(this.settingConflicts) &&
StepWarning(this.settingConflictWarning)}
</StepHeader>
<StepContent className={className}>
<Row>
<Col md={12}>

View File

@ -4,7 +4,8 @@ import {
StepWrapper,
StepHeader,
StepContent,
StepHeaderProps
StepHeaderProps,
StepWarning
} from "../index";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
@ -52,3 +53,11 @@ describe("<StepContent />", () => {
expect(div.hasClass("step-class")).toBeTruthy();
});
});
describe("<StepWarning />", () => {
it("renders", () => {
const wrapper = mount(StepWarning("warning"));
expect(wrapper.find("i").hasClass("fa-exclamation-triangle")).toBeTruthy();
expect(wrapper.text()).toContain("Hardware setting conflict.");
});
});

View File

@ -1,3 +1,4 @@
export * from "./step_wrapper";
export * from "./step_header";
export * from "./step_content";
export * from "./step_warning";

View File

@ -0,0 +1,18 @@
import * as React from "react";
import { t } from "i18next";
import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core";
export function StepWarning(warning: string) {
return <div className="step-warning">
<Popover
position={Position.LEFT_TOP}
interactionKind={PopoverInteractionKind.HOVER}
popoverClassName={"help"} >
<i className="fa fa-exclamation-triangle" />
<div>
{warning}
</div>
</Popover>
&nbsp;{t("Hardware setting conflict.")}
</div>;
}