Merge pull request #1648 from FarmBot/new_steps

Part I of New Sequence Steps
log_problems_iii
Rick Carlino 2020-01-02 11:40:15 -06:00 committed by GitHub
commit 19798b894f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 391 additions and 41 deletions

View File

@ -284,6 +284,8 @@ export namespace ToolTips {
export const TAKE_PHOTO =
trim(`Snaps a photo using the device camera. Select the camera type
on the Device page.`);
export const EMERGENCY_LOCK =
trim(`Stops a device from moving until it is unlocked by a user.`);
export const SELECT_A_CAMERA =
trim(`Select a camera on the Device page to take photos.`);
@ -294,6 +296,9 @@ export namespace ToolTips {
For example, you can mark a plant as "planted" during a seeding
sequence or delete a weed after removing it.`);
export const REBOOT =
trim(`Power cycle FarmBot's onboard computer or microcontroller.`);
export const SET_SERVO_ANGLE =
trim(`Move a servo to the provided angle.`);

View File

@ -0,0 +1,58 @@
jest.mock("../../api/crud", () => {
return { editStep: jest.fn() };
});
import { TileReboot, editTheRebootStep } from "../step_tiles/tile_reboot";
import { render } from "enzyme";
import React from "react";
import { StepParams } from "../interfaces";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { editStep } from "../../api/crud";
const fakeProps = (): StepParams => {
const currentSequence = fakeSequence();
const resources = buildResourceIndex().index;
return {
currentSequence,
currentStep: {
kind: "reboot",
args: {
package: "farmbot_os"
}
},
dispatch: jest.fn(),
index: 1,
resources,
confirmStepDeletion: false,
};
};
describe("<TileReboot/>", () => {
it("renders", () => {
const el = render(<TileReboot {...fakeProps()} />);
const verbiage = el.text();
expect(verbiage).toContain("Entire system");
expect(verbiage).toContain("Just the Arduino");
});
it("crashes if the step is of the wrong `kind`", () => {
const props = fakeProps();
props.currentStep = { kind: "sync", args: {} };
const boom = () => TileReboot(props);
expect(boom).toThrowError();
});
it("edits the reboot step", () => {
const props = fakeProps();
const editFn = editTheRebootStep(props);
editFn("arduino_firmware");
expect(props.dispatch).toHaveBeenCalled();
expect(editStep).toHaveBeenCalledWith({
step: props.currentStep,
index: props.index,
sequence: props.currentSequence,
executor: expect.any(Function),
});
});
});

View File

@ -48,6 +48,16 @@ export function StepButtonCluster(props: StepButtonProps) {
color="orange">
{t("CONTROL PERIPHERAL")}
</StepButton>,
<StepButton {...commonStepProps}
step={{
kind: "toggle_pin",
args: {
pin_number: 4
}
}}
color="yellow">
{t("TOGGLE PERIPHERAL")}
</StepButton>,
<StepButton {...commonStepProps}
step={{
kind: "read_pin",
@ -60,6 +70,17 @@ export function StepButtonCluster(props: StepButtonProps) {
color="yellow">
{t("READ SENSOR")}
</StepButton>,
<StepButton {...commonStepProps}
step={{
kind: "set_servo_angle",
args: {
pin_number: 4,
pin_value: 90
}
}}
color="blue">
{t("CONTROL SERVO")}
</StepButton>,
<StepButton {...commonStepProps}
step={{
kind: "wait",
@ -79,6 +100,16 @@ export function StepButtonCluster(props: StepButtonProps) {
color="red">
{t("SEND MESSAGE")}
</StepButton>,
<StepButton {...commonStepProps}
step={{ kind: "emergency_lock", args: {} }}
color="brown">
{t("EMERGENCY LOCK")}
</StepButton>,
<StepButton {...commonStepProps}
step={{ kind: "reboot", args: { package: "farmbot_os" } }}
color="brown">
{t("REBOOT")}
</StepButton>,
<StepButton{...commonStepProps}
step={{
kind: "find_home",

View File

@ -5,12 +5,7 @@ jest.mock("../../../api/crud", () => ({
import { remove, move, splice, renderCeleryNode } from "../index";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { overwrite } from "../../../api/crud";
import {
Wait, If, ExecuteScript, Execute, FindHome, MoveAbsolute, SendMessage,
TakePhoto, SetServoAngle, TogglePin, Zero, Calibrate, Home, Reboot,
CheckUpdates, FactoryReset, Sync, DumpInfo, PowerOff, ReadStatus,
EmergencyLock, EmergencyUnlock, InstallFirstPartyFarmware
} from "farmbot";
import { SequenceBodyItem, Wait } from "farmbot";
import { mount } from "enzyme";
import { StepParams, MessageType } from "../../interfaces";
import { emptyState } from "../../../resources/reducer";
@ -96,30 +91,105 @@ describe("renderCeleryNode()", () => {
confirmStepDeletion: false,
});
const TEST_DATA = [
interface TestData {
node: SequenceBodyItem;
expected: string;
}
const TEST_DATA: TestData[] = [
{
expected: "MarkPlantasx = 300",
node: {
kind: "resource_update",
args: {
resource_id: 23,
resource_type: "Plant",
label: "x",
value: 300
}
}
},
{
expected: "System",
node: {
kind: "check_updates",
args: {
package: "farmbot_os"
}
}
},
{
expected: "System",
node: {
kind: "factory_reset",
args: {
package: "farmbot_os"
}
}
},
{
expected: "Unlocking a device requires user intervention",
node: {
kind: "emergency_lock",
args: {}
}
},
{
expected: "Unable to properly display this step",
node: {
kind: "power_off",
args: {}
}
},
{
expected: "Unable to properly display this step",
node: { kind: "read_status", args: {} }
},
{
expected: "",
node: {
kind: "install_first_party_farmware",
args: {}
}
},
{
node: {
kind: "_if",
args: {
lhs: "pin0", op: "is", rhs: 0, _then: { kind: "nothing", args: {} },
lhs: "pin0",
op: "is",
rhs: 0,
_then: { kind: "nothing", args: {} },
_else: { kind: "nothing", args: {} }
}
} as If, expected: "Then Execute"
},
expected: "Then Execute"
},
{
node: {
kind: "execute_script",
args: { label: "farmware-to-execute" }
} as ExecuteScript, expected: "Manual Input"
},
expected: "Manual Input"
},
{
node: { kind: "execute", args: { sequence_id: 0 } } as Execute,
node: {
kind: "execute",
args: {
sequence_id: 0
}
},
expected: "Select a sequence"
},
{
node: {
kind: "find_home", args: { speed: 100, axis: "all" }
} as FindHome, expected: "Find x"
kind: "find_home",
args: {
speed: 100,
axis: "all"
}
},
expected: "Find x"
},
{
node: {
@ -129,7 +199,8 @@ describe("renderCeleryNode()", () => {
speed: 100,
offset: { kind: "coordinate", args: { x: 4, y: 5, z: 6 } }
}
} as MoveAbsolute, expected: "x-Offsety-Offsetz-OffsetSpeed (%)"
},
expected: "x-Offsety-Offsetz-OffsetSpeed (%)"
},
{
node: {
@ -138,70 +209,124 @@ describe("renderCeleryNode()", () => {
message: "send this message",
message_type: MessageType.info
}
} as SendMessage, expected: "Message"
},
expected: "Message"
},
{ node: { kind: "take_photo", args: {} } as TakePhoto, expected: "Photo" },
{
node: { kind: "wait", args: { milliseconds: 100 } } as Wait,
node: {
kind: "take_photo",
args: {}
},
expected: "Photo"
},
{
node: {
kind: "wait",
args: {
milliseconds: 100
}
},
expected: "milliseconds"
},
{
node: {
kind: "set_servo_angle",
args: { pin_number: 4, pin_value: 90, }
} as SetServoAngle, expected: "Servo"
args: {
pin_number: 4,
pin_value: 90,
}
},
expected: "Servo"
},
{
node: { kind: "toggle_pin", args: { pin_number: 13 } } as TogglePin,
node: {
kind: "toggle_pin",
args: {
pin_number: 13
}
},
expected: "Pin"
},
{
node: { kind: "zero", args: { axis: "all" } } as Zero,
node: { kind: "zero", args: { axis: "all" } },
expected: "Zero x"
},
{
node: { kind: "calibrate", args: { axis: "all" } } as Calibrate,
node: { kind: "calibrate", args: { axis: "all" } },
expected: "Calibrate x"
},
{
node: { kind: "home", args: { axis: "all", speed: 100, } } as Home,
node: { kind: "home", args: { axis: "all", speed: 100, } },
expected: "Home x"
},
{
node: { kind: "reboot", args: { package: "farmbot_os" } } as Reboot,
node: { kind: "reboot", args: { package: "farmbot_os" } },
expected: "System"
},
{
node: {
kind: "check_updates", args: { package: "farmbot_os" }
} as CheckUpdates,
},
expected: "System"
},
{
node: {
kind: "factory_reset", args: { package: "farmbot_os" }
} as FactoryReset,
},
expected: "System"
},
{ node: { kind: "sync", args: {} } as Sync, expected: "" },
{ node: { kind: "dump_info", args: {} } as DumpInfo, expected: "" },
{ node: { kind: "power_off", args: {} } as PowerOff, expected: "" },
{ node: { kind: "read_status", args: {} } as ReadStatus, expected: "" },
{
node: { kind: "emergency_lock", args: {} } as EmergencyLock,
node: {
kind: "sync",
args: {}
},
expected: ""
},
{
node: { kind: "emergency_unlock", args: {} } as EmergencyUnlock,
node: {
kind: "dump_info",
args: {}
},
expected: ""
},
{
node: {
kind: "power_off",
args: {}
},
expected: ""
},
{
node: {
kind: "read_status",
args: {}
},
expected: ""
},
{
node: { kind: "emergency_lock", args: {} },
expected: ""
},
{
node: { kind: "emergency_unlock", args: {} },
expected: ""
},
{
node: {
kind: "install_first_party_farmware", args: {}
} as InstallFirstPartyFarmware, expected: ""
},
expected: ""
},
{
node: {
kind: "unknown",
args: {
unknown: 0
}
// tslint:disable-next-line:no-any
} as any,
expected: "unknown"
},
// tslint:disable-next-line:no-any
{ node: { kind: "unknown", args: { unknown: 0 } } as any, expected: "unknown" },
];
it("renders correct step", () => {
@ -209,7 +334,8 @@ describe("renderCeleryNode()", () => {
const p = fakeProps();
p.currentStep = test.node;
const step = renderCeleryNode(p);
expect(mount(step).text()).toContain(test.expected);
const verbiage = mount(step).text().toLowerCase();
expect(verbiage).toContain(test.expected.toLowerCase());
});
});
});

View File

@ -31,6 +31,8 @@ import { TileCalibrate } from "./tile_calibrate";
import { TileMoveHome } from "./tile_move_home";
import { t } from "../../i18next_wrapper";
import { TileAssertion } from "./tile_assertion";
import { TileEmergencyStop } from "./tile_emergency_stop";
import { TileReboot } from "./tile_reboot";
interface MoveParams {
step: Step;
@ -153,13 +155,15 @@ export function renderCeleryNode(props: StepParams) {
case "zero": return <TileSetZero {...props} />;
case "calibrate": return <TileCalibrate {...props} />;
case "home": return <TileMoveHome {...props} />;
case "reboot": case "check_updates": case "factory_reset":
return <TileFirmwareAction {...props} />;
case "sync": case "dump_info": case "power_off": case "read_status":
case "emergency_unlock": case "emergency_lock":
case "reboot": return <TileReboot {...props} />;
case "emergency_lock": return <TileEmergencyStop {...props} />;
case "install_first_party_farmware": return <TileSystemAction {...props} />;
case "assertion": return <TileAssertion {...props} />;
default: return <TileUnknown {...props} />;
case "check_updates":
case "factory_reset":
return <TileFirmwareAction {...props} />;
default:
return <TileUnknown {...props} />;
}
}

View File

@ -0,0 +1,30 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui";
import { Col, Row } from "../../ui/index";
import { t } from "../../i18next_wrapper";
export function TileEmergencyStop(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props;
const className = "take-photo-step";
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.EMERGENCY_LOCK}
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
index={index}
confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}>
<Row>
<Col xs={12}>
<p>
{t("Unlocking a device requires user intervention")}
</p>
</Col>
</Row>
</StepContent>
</StepWrapper>;
}

View File

@ -0,0 +1,96 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import { t } from "../../i18next_wrapper";
import { Row, Col } from "../../ui";
import { ALLOWED_PACKAGES, SequenceBodyItem, Reboot } from "farmbot";
import { editStep } from "../../api/crud";
type StringMap = Record<string, string>;
interface MultiChoiceRadioProps<T extends StringMap> {
uuid: string;
choices: T;
currentChoice: keyof T;
onChange(key: keyof T): void;
}
const MultiChoiceRadio =
<T extends StringMap>(props: MultiChoiceRadioProps<T>) => {
const choices = Object.keys(props.choices);
return <Row>
<Col xs={12}>
<div className="bottom-content">
<div className="channel-fields">
<form>
{choices.map((choice, i) =>
<div key={`${props.uuid} ${i}`} style={{ display: "inline" }}>
<label>
<input type="radio"
value={choice}
onChange={() => props.onChange(choice)}
checked={props.currentChoice === choice} />
{t(props.choices[choice])}
</label>
</div>)}
</form>
</div>
</div>
</Col>
</Row>;
};
const PACKAGE_CHOICES: Record<ALLOWED_PACKAGES, string> = {
"arduino_firmware": "Just the Arduino",
"farmbot_os": "Entire system"
};
function assertReboot(x: SequenceBodyItem): asserts x is Reboot {
if (x.kind !== "reboot") {
throw new Error("Impossible");
}
}
type RELEVANT_KEYS = "currentStep" | "currentSequence" | "index" | "dispatch";
type RebootEditProps = Pick<StepParams, RELEVANT_KEYS>;
export const rebootExecutor =
(pkg: ALLOWED_PACKAGES) => (step: SequenceBodyItem) => {
assertReboot(step);
step.args.package = pkg;
};
export const editTheRebootStep =
(props: RebootEditProps) => (pkg: ALLOWED_PACKAGES) => {
const { currentStep, index, currentSequence } = props;
props.dispatch(editStep({
step: currentStep,
index,
sequence: currentSequence,
executor: rebootExecutor(pkg),
}));
};
export function TileReboot(props: StepParams) {
const { dispatch, currentStep, index, currentSequence } = props;
const className = "set-zero-step";
assertReboot(currentStep);
return <StepWrapper>
<StepHeader
className={className}
helpText={ToolTips.REBOOT}
currentSequence={currentSequence}
currentStep={currentStep}
dispatch={dispatch}
index={index}
confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={className}>
<MultiChoiceRadio
uuid={currentSequence.uuid + index}
choices={PACKAGE_CHOICES}
currentChoice={currentStep.args.package as ALLOWED_PACKAGES}
onChange={editTheRebootStep(props)} />
</StepContent>
</StepWrapper>;
}