add firmware restart button

pull/995/head
gabrielburnworth 2018-09-24 12:34:38 -07:00
parent 3d8a06bacf
commit e9f305e543
10 changed files with 144 additions and 85 deletions

View File

@ -441,6 +441,9 @@ export namespace Content {
trim(`This will restart FarmBot's Raspberry Pi and controller
software.`);
export const RESTART_FIRMWARE =
trim(`Restart the Farmduino or Arduino firmware.`);
export const OS_AUTO_UPDATE =
trim(`When enabled, FarmBot OS will periodically check for, download,
and install updates automatically.`);

View File

@ -1,5 +1,5 @@
const mockDevice = {
send: jest.fn(() => { return Promise.resolve(); })
readPin: jest.fn(() => { return Promise.resolve(); })
};
jest.mock("../../../device", () => ({
getDevice: () => (mockDevice)
@ -55,27 +55,20 @@ describe("<SensorList/>", function () {
});
const expectedPayload = (pin_number: number, pin_mode: 0 | 1) =>
expect.objectContaining({
kind: "rpc_request",
args: expect.objectContaining({ label: expect.any(String) }),
body: [expect.objectContaining({
kind: "read_pin",
args: {
pin_number,
label: `pin${pin_number}`,
pin_mode
}
})]
({
pin_number,
label: `pin${pin_number}`,
pin_mode
});
it("reads pins", () => {
const wrapper = mount(<SensorList {...fakeProps()} />);
const toggle = wrapper.find("button");
toggle.first().simulate("click");
expect(mockDevice.send).toHaveBeenCalledWith(expectedPayload(51, 1));
expect(mockDevice.readPin).toHaveBeenCalledWith(expectedPayload(51, 1));
toggle.last().simulate("click");
expect(mockDevice.send).toHaveBeenLastCalledWith(expectedPayload(50, 0));
expect(mockDevice.send).toHaveBeenCalledTimes(2);
expect(mockDevice.readPin).toHaveBeenLastCalledWith(expectedPayload(50, 0));
expect(mockDevice.readPin).toHaveBeenCalledTimes(2);
});
it("pins toggles are disabled", () => {
@ -85,6 +78,6 @@ describe("<SensorList/>", function () {
const toggle = wrapper.find("button");
toggle.first().simulate("click");
toggle.last().simulate("click");
expect(mockDevice.send).not.toHaveBeenCalled();
expect(mockDevice.readPin).not.toHaveBeenCalled();
});
});

View File

@ -3,6 +3,7 @@ const mockDevice = {
powerOff: jest.fn(() => { return Promise.resolve(); }),
resetOS: jest.fn(),
reboot: jest.fn(() => { return Promise.resolve(); }),
send: jest.fn(() => { return Promise.resolve(); }),
checkArduinoUpdates: jest.fn(() => { return Promise.resolve(); }),
emergencyLock: jest.fn(() => { return Promise.resolve(); }),
emergencyUnlock: jest.fn(() => { return Promise.resolve(); }),
@ -35,7 +36,9 @@ import {
fakeSequence, fakeFbosConfig, fakeFirmwareConfig
} from "../../__test_support__/fake_state/resources";
import { fakeState } from "../../__test_support__/fake_state";
import { changeStepSize, resetNetwork, resetConnectionInfo } from "../actions";
import {
changeStepSize, resetNetwork, resetConnectionInfo, commandErr
} from "../actions";
import { Actions } from "../../constants";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { API } from "../../api/index";
@ -82,6 +85,14 @@ describe("reboot()", function () {
});
});
describe("restartFirmware()", function () {
it("calls restartFirmware", async () => {
await actions.restartFirmware();
expect(mockDevice.send).toHaveBeenCalled();
expect(success).toHaveBeenCalled();
});
});
describe("emergencyLock() / emergencyUnlock", function () {
it("calls emergencyLock", () => {
actions.emergencyLock();
@ -249,6 +260,18 @@ describe("isLog()", function () {
expect(actions.isLog({})).toBe(false);
expect(actions.isLog({ message: "foo" })).toBe(true);
});
it("filters sensitive logs", () => {
expect(() => actions.isLog({ message: "NERVESPSKWPASSWORD" }))
.toThrowError(/Refusing to display log/);
});
});
describe("commandErr()", () => {
it("sends toast", () => {
commandErr()();
expect(error).toHaveBeenCalledWith("Command failed");
});
});
describe("toggleControlPanel()", function () {

View File

@ -91,6 +91,16 @@ export function reboot() {
.then(commandOK(noun), commandErr(noun));
}
export function restartFirmware() {
const noun = "Restart Firmware";
getDevice() // TODO: add `restartFirmware()` to FBJS
.send(rpcRequest([{
kind: "reboot",
args: { package: "arduino_firmware" }
}]))
.then(commandOK(noun), commandErr(noun));
}
export function emergencyLock() {
const noun = "Emergency stop";
getDevice()
@ -297,9 +307,7 @@ export function pinToggle(pin_number: number) {
export function readPin(pin_number: number, label: string, pin_mode: number) {
const noun = "Read pin";
return getDevice()
.send(rpcRequest([{
kind: "read_pin", args: { pin_number, label, pin_mode }
}]))
.readPin({ pin_number, label, pin_mode })
.then(_.noop, commandErr(noun));
}

View File

@ -1,5 +1,6 @@
const mockDevice = {
updateConfig: jest.fn(() => { return Promise.resolve(); }),
send: jest.fn(() => { return Promise.resolve(); }),
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
@ -12,6 +13,7 @@ import { PowerAndResetProps } from "../interfaces";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { fakeState } from "../../../../__test_support__/fake_state";
import { clickButton } from "../../../../__test_support__/helpers";
describe("<PowerAndReset/>", () => {
const fakeProps = (): PowerAndResetProps => {
@ -34,6 +36,9 @@ describe("<PowerAndReset/>", () => {
"Automatic Factory Reset", "Connection Attempt Period"]
.map(string => expect(wrapper.text().toLowerCase())
.toContain(string.toLowerCase()));
["Restart Firmware", "Change Ownership"]
.map(string => expect(wrapper.text().toLowerCase())
.not.toContain(string.toLowerCase()));
});
it("closed", () => {
@ -61,8 +66,35 @@ describe("<PowerAndReset/>", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
wrapper.find("button").at(3).simulate("click");
clickButton(wrapper, 3, "yes");
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ disable_factory_reset: true });
});
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());
clickButton(wrapper, 2, "restart");
expect(mockDevice.send).toHaveBeenCalledWith(
expect.objectContaining({
kind: "rpc_request",
args: expect.objectContaining({ label: expect.any(String) }),
body: [expect.objectContaining({
kind: "reboot", args: { package: "arduino_firmware" }
})]
}));
});
it("shows change ownership button", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
p.shouldDisplay = () => true;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Change Ownership".toLowerCase());
});
});

View File

@ -0,0 +1,37 @@
import * as React from "react";
import { Row, Col } from "../../../ui";
import { t } from "i18next";
import { ColWidth } from "../farmbot_os_settings";
export interface FbosButtonRowProps {
botOnline: boolean;
label: string;
description: string;
buttonText: string;
color: string;
action: () => void;
}
export const FbosButtonRow = (props: FbosButtonRowProps) => {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t(props.label)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(props.description)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className={`fb-button ${props.color}`}
type="button"
onClick={props.action}
disabled={!props.botOnline}>
{t(props.buttonText)}
</button>
</Col>
</Row>;
};

View File

@ -1,13 +1,14 @@
import * as React from "react";
import { Header } from "../hardware_settings/header";
import { Collapse, Popover, Position } from "@blueprintjs/core";
import { RestartRow } from "./restart_row";
import { ShutdownRow } from "./shutdown_row";
import { FactoryResetRow } from "./factory_reset_row";
import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form";
import { t } from "i18next";
import { Feature } from "../../interfaces";
import { FbosButtonRow } from "./fbos_button_row";
import { Content } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions";
export function PowerAndReset(props: PowerAndResetProps) {
const { dispatch, sourceFbosConfig, shouldDisplay, botOnline } = props;
@ -21,8 +22,28 @@ export function PowerAndReset(props: PowerAndResetProps) {
dispatch={dispatch} />
</div>
<Collapse isOpen={!!power_and_reset}>
<RestartRow botOnline={botOnline} />
<ShutdownRow botOnline={botOnline} />
<FbosButtonRow
botOnline={botOnline}
label={t("RESTART FARMBOT")}
description={Content.RESTART_FARMBOT}
buttonText={t("RESTART")}
color={"yellow"}
action={reboot} />
<FbosButtonRow
botOnline={botOnline}
label={t("SHUTDOWN FARMBOT")}
description={Content.SHUTDOWN_FARMBOT}
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} />}
<FactoryResetRow
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig}

View File

@ -1,30 +0,0 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { t } from "i18next";
import { Content } from "../../../constants";
import { reboot } from "../../actions";
import { ColWidth } from "../farmbot_os_settings";
export function RestartRow({ botOnline }: { botOnline: boolean }) {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("RESTART FARMBOT")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.RESTART_FARMBOT)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className="fb-button yellow"
type="button"
onClick={reboot}
disabled={!botOnline}>
{t("RESTART")}
</button>
</Col>
</Row>;
}

View File

@ -1,30 +0,0 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { t } from "i18next";
import { Content } from "../../../constants";
import { powerOff } from "../../actions";
import { ColWidth } from "../farmbot_os_settings";
export function ShutdownRow({ botOnline }: { botOnline: boolean }) {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("SHUTDOWN FARMBOT")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.SHUTDOWN_FARMBOT)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className="fb-button red"
type="button"
onClick={powerOff}
disabled={!botOnline}>
{t("SHUTDOWN")}
</button>
</Col>
</Row>;
}

View File

@ -65,6 +65,8 @@ export enum Feature {
diagnostic_dumps = "diagnostic_dumps",
rpi_led_control = "rpi_led_control",
mark_as_step = "mark_as_step",
firmware_restart = "firmware_restart",
api_farmware_installations = "api_farmware_installations",
}
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;