Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into resource_service
commit
9582c0a590
|
@ -15,6 +15,7 @@ import { ExecutableType } from "../../farm_designer/interfaces";
|
|||
import { fakeResource } from "../fake_resource";
|
||||
import { emptyToolSlot } from "../../tools/components/empty_tool_slot";
|
||||
import { FirmwareConfig } from "../../config_storage/firmware_configs";
|
||||
import { PinBindingType } from "../../devices/pin_bindings/interfaces";
|
||||
|
||||
export let resources: Everything["resources"] = buildResourceIndex();
|
||||
let idCounter = 1;
|
||||
|
@ -164,7 +165,7 @@ export function fakePinBinding(): TaggedPinBinding {
|
|||
id: idCounter++,
|
||||
pin_num: 10,
|
||||
sequence_id: 1,
|
||||
binding_type: "standard"
|
||||
binding_type: PinBindingType.standard,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -3,5 +3,6 @@ jest.mock("farmbot-toastr", () => ({
|
|||
init: jest.fn(),
|
||||
success: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn()
|
||||
error: jest.fn(),
|
||||
warning: jest.fn()
|
||||
}));
|
||||
|
|
|
@ -302,6 +302,7 @@ a {
|
|||
}
|
||||
.bindings-list {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
const mockDevice = {
|
||||
registerGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
unregisterGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
};
|
||||
jest.mock("../../../device", () => ({
|
||||
getDevice: () => (mockDevice)
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
destroy: jest.fn(),
|
||||
initSave: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { PinBindings, PinBindingsProps } from "../pin_bindings";
|
||||
import { mount } from "enzyme";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { TaggedSequence } from "../../../resources/tagged_resources";
|
||||
import {
|
||||
fakeSequence, fakePinBinding
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { destroy, initSave } from "../../../api/crud";
|
||||
|
||||
describe("<PinBindings/>", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function fakeProps(): PinBindingsProps {
|
||||
const fakeResources: TaggedSequence[] = [fakeSequence(), fakeSequence()];
|
||||
fakeResources[0].body.id = 1;
|
||||
fakeResources[0].body.name = "Sequence 1";
|
||||
fakeResources[1].body.id = 2;
|
||||
fakeResources[1].body.name = "Sequence 2";
|
||||
const resources = buildResourceIndex(fakeResources).index;
|
||||
|
||||
bot.hardware.gpio_registry = {
|
||||
10: "1",
|
||||
11: "2"
|
||||
};
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
resources: resources,
|
||||
botToMqttStatus: "up",
|
||||
shouldDisplay: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<PinBindings {...fakeProps()} />);
|
||||
["pin bindings", "pin number", "none", "bind"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
["pi gpio 10", "sequence 1", "pi gpio 11", "sequence 2"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
expect(wrapper.find("input").length).toBe(1);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toBe(4);
|
||||
});
|
||||
|
||||
it("unregisters pin: bot", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => x(jest.fn()));
|
||||
const wrapper = mount(<PinBindings {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
buttons.first().simulate("click");
|
||||
expect(mockDevice.unregisterGpio).toHaveBeenCalledWith({
|
||||
pin_number: 10
|
||||
});
|
||||
});
|
||||
|
||||
it("unregisters pin: api", () => {
|
||||
const p = fakeProps();
|
||||
const s = fakeSequence();
|
||||
s.body.id = 1;
|
||||
p.resources = buildResourceIndex([fakePinBinding(), s]).index;
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<PinBindings {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
buttons.first().simulate("click");
|
||||
expect(mockDevice.unregisterGpio).not.toHaveBeenCalled();
|
||||
expect(destroy).toHaveBeenCalledWith(expect.stringContaining("PinBinding"));
|
||||
});
|
||||
|
||||
it("registers pin: bot", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => x(jest.fn()));
|
||||
const wrapper = mount(<PinBindings {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
|
||||
buttons.last().simulate("click");
|
||||
expect(mockDevice.registerGpio).toHaveBeenCalledWith({
|
||||
pin_number: 1, sequence_id: 2
|
||||
});
|
||||
});
|
||||
|
||||
it("registers pin: api", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<PinBindings {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
|
||||
buttons.last().simulate("click");
|
||||
expect(mockDevice.registerGpio).not.toHaveBeenCalled();
|
||||
const expectedResult = expect.objectContaining({
|
||||
kind: "PinBinding",
|
||||
body: {
|
||||
pin_num: 1,
|
||||
sequence_id: 2,
|
||||
binding_type: "standard"
|
||||
}
|
||||
});
|
||||
expect(initSave).toHaveBeenCalledWith(expectedResult);
|
||||
});
|
||||
|
||||
it("sets sequence id", () => {
|
||||
const p = fakeProps();
|
||||
const s = p.resources.references[p.resources.byKind.Sequence[0]];
|
||||
const id = s && s.body.id;
|
||||
const wrapper = mount<PinBindings>(<PinBindings {...p} />);
|
||||
expect(wrapper.instance().state.sequenceIdInput).toEqual(undefined);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.changeSelection({ label: "label", value: id });
|
||||
expect(wrapper.instance().state.sequenceIdInput).toEqual(id);
|
||||
});
|
||||
|
||||
it("sets pin", () => {
|
||||
const wrapper = mount<PinBindings>(<PinBindings {...fakeProps()} />);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.setSelectedPin(10);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
instance.setSelectedPin(99);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
instance.setSelectedPin(5);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(5);
|
||||
});
|
||||
});
|
|
@ -1,269 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import * as _ from "lodash";
|
||||
import {
|
||||
Widget, WidgetBody, WidgetHeader,
|
||||
Row, Col,
|
||||
BlurableInput,
|
||||
DropDownItem
|
||||
} from "../../ui/index";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { BotState, ShouldDisplay, Feature } from "../interfaces";
|
||||
import { registerGpioPin, unregisterGpioPin } from "../actions";
|
||||
import { findSequenceById, selectAllPinBindings } from "../../resources/selectors";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
import { MustBeOnline } from "../must_be_online";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
import { RpiGpioDiagram, gpio } from "./rpi_gpio_diagram";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { NetworkState } from "../../connectivity/interfaces";
|
||||
import { SequenceSelectBox } from "../../sequences/sequence_select_box";
|
||||
import { initSave, destroy } from "../../api/crud";
|
||||
import { TaggedPinBinding, SpecialStatus } from "../../resources/tagged_resources";
|
||||
|
||||
/**
|
||||
* PROBLEM SCENARIO: New FarmBot boots up. It does not have any sequences yet,
|
||||
* because it's never talked with the API. You _must_ be able to E-stop the
|
||||
* device via button press.
|
||||
*
|
||||
* SOLUTION: Bake some sane defaults (heretofore "builtin sequences" and
|
||||
* "builtin bindings") into the FBOS image. Make the
|
||||
*
|
||||
* NEW PROBLEM: We need a way to map FBOS friendly magic numbers to human
|
||||
* friendly labels on the UI layer.
|
||||
*
|
||||
* SOLUTION: Hard code a magic number mapping in to the FE.
|
||||
*
|
||||
* PRECAUTIONS:
|
||||
* + Numbers can never change (will break old FBOS versions)
|
||||
* + If we ever need to share this mapping, keep it in the API as a constant.
|
||||
* + Numbers will cause runtime errors if sent to the API. Best to keep these
|
||||
* numbers local to FE/FBOS.
|
||||
*/
|
||||
export const magicNumbers = {
|
||||
sequences: {
|
||||
emergency_lock: -1,
|
||||
emergency_unlock: -2,
|
||||
sync: -3,
|
||||
reboot: -4,
|
||||
power_off: -5,
|
||||
},
|
||||
pin_bindings: {
|
||||
emergency_lock: -1,
|
||||
emergency_unlock: -2
|
||||
}
|
||||
};
|
||||
|
||||
export interface PinBindingsProps {
|
||||
bot: BotState;
|
||||
dispatch: Function;
|
||||
botToMqttStatus: NetworkState;
|
||||
resources: ResourceIndex;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface PinBindingsState {
|
||||
isEditing: boolean;
|
||||
pinNumberInput: number | undefined;
|
||||
sequenceIdInput: number | undefined;
|
||||
}
|
||||
|
||||
enum ColumnWidth {
|
||||
pin = 4,
|
||||
sequence = 7,
|
||||
button = 1
|
||||
}
|
||||
|
||||
export class PinBindings
|
||||
extends React.Component<PinBindingsProps, PinBindingsState> {
|
||||
constructor(props: PinBindingsProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
pinNumberInput: undefined,
|
||||
sequenceIdInput: undefined
|
||||
};
|
||||
}
|
||||
|
||||
get pinBindings(): {
|
||||
pin_number: number, sequence_id: number, uuid?: string
|
||||
}[] {
|
||||
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
|
||||
return selectAllPinBindings(this.props.resources)
|
||||
.map(x => {
|
||||
const { body } = x;
|
||||
const sequence_id = // TODO: Handle special bindings.
|
||||
body.binding_type == "standard" ? body.sequence_id : 0;
|
||||
return {
|
||||
pin_number: x.body.pin_num,
|
||||
sequence_id,
|
||||
uuid: x.uuid
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const { gpio_registry } = this.props.bot.hardware;
|
||||
return Object.entries(gpio_registry || {})
|
||||
.map(([pin_number, sequence_id]) => {
|
||||
return {
|
||||
pin_number: parseInt(pin_number),
|
||||
sequence_id: parseInt(sequence_id || "")
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
changeSelection = (input: DropDownItem) => {
|
||||
this.setState({ sequenceIdInput: parseInt(input.value as string) });
|
||||
}
|
||||
|
||||
setSelectedPin = (pin: number | undefined) => {
|
||||
if (!_.includes(this.boundPins, pin)) {
|
||||
if (_.includes(_.flattenDeep(gpio), pin)) {
|
||||
this.setState({ pinNumberInput: pin });
|
||||
} else {
|
||||
error("Invalid Raspberry Pi GPIO pin number.");
|
||||
}
|
||||
} else {
|
||||
error("Raspberry Pi GPIO pin already bound.");
|
||||
}
|
||||
}
|
||||
|
||||
taggedPinBinding =
|
||||
(pin_num: number, sequence_id: number): TaggedPinBinding => {
|
||||
return {
|
||||
uuid: "WILL_BE_CHANGED_BY_REDUCER",
|
||||
specialStatus: SpecialStatus.SAVED,
|
||||
kind: "PinBinding",
|
||||
body: { pin_num, sequence_id, binding_type: "standard" }
|
||||
};
|
||||
}
|
||||
|
||||
bindPin = () => {
|
||||
const { pinNumberInput, sequenceIdInput } = this.state;
|
||||
if (pinNumberInput && sequenceIdInput) {
|
||||
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
|
||||
this.props.dispatch(initSave(
|
||||
this.taggedPinBinding(pinNumberInput, sequenceIdInput)));
|
||||
} else {
|
||||
this.props.dispatch(registerGpioPin({
|
||||
pin_number: pinNumberInput,
|
||||
sequence_id: sequenceIdInput
|
||||
}));
|
||||
}
|
||||
this.setState({
|
||||
pinNumberInput: undefined,
|
||||
sequenceIdInput: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deleteBinding = (pin: number, uuid?: string) => {
|
||||
if (this.props.shouldDisplay(Feature.api_pin_bindings)) {
|
||||
this.props.dispatch(destroy(uuid || ""));
|
||||
} else {
|
||||
this.props.dispatch(unregisterGpioPin(pin));
|
||||
}
|
||||
}
|
||||
|
||||
get boundPins(): number[] | undefined {
|
||||
return this.pinBindings.map(x => x.pin_number);
|
||||
}
|
||||
|
||||
currentBindingsList = () => {
|
||||
const { resources } = this.props;
|
||||
return <div className={"bindings-list"}>
|
||||
{this.pinBindings
|
||||
.map(x => {
|
||||
const { pin_number, sequence_id } = x;
|
||||
return <Row key={`pin_${pin_number}_binding`}>
|
||||
<Col xs={ColumnWidth.pin}>
|
||||
{`Pi GPIO ${pin_number}`}
|
||||
</Col>
|
||||
<Col xs={ColumnWidth.sequence}>
|
||||
{sequence_id ? findSequenceById(
|
||||
resources, sequence_id).body.name : ""}
|
||||
</Col>
|
||||
<Col xs={ColumnWidth.button}>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={() => this.deleteBinding(pin_number, x.uuid)}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
pinBindingInputGroup = () => {
|
||||
const { pinNumberInput, sequenceIdInput } = this.state;
|
||||
return <Row>
|
||||
<Col xs={ColumnWidth.pin}>
|
||||
<Row>
|
||||
<Col xs={1}>
|
||||
<Popover position={Position.TOP}>
|
||||
<i className="fa fa-th-large" />
|
||||
<RpiGpioDiagram
|
||||
boundPins={this.boundPins}
|
||||
setSelectedPin={this.setSelectedPin}
|
||||
selectedPin={this.state.pinNumberInput} />
|
||||
</Popover>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<BlurableInput
|
||||
onCommit={(e) =>
|
||||
this.setSelectedPin(parseInt(e.currentTarget.value))}
|
||||
name="pin_number"
|
||||
value={_.isNumber(pinNumberInput) ? pinNumberInput : ""}
|
||||
type="number" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={ColumnWidth.sequence}>
|
||||
<SequenceSelectBox
|
||||
key={sequenceIdInput}
|
||||
onChange={this.changeSelection}
|
||||
resources={this.props.resources}
|
||||
sequenceId={sequenceIdInput} />
|
||||
</Col>
|
||||
<Col xs={ColumnWidth.button}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={() => { this.bindPin(); }} >
|
||||
{t("BIND")}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Widget className="pin-bindings-widget">
|
||||
<WidgetHeader
|
||||
title={t("Pin Bindings")}
|
||||
helpText={ToolTips.PIN_BINDINGS} />
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.bot.hardware.informational_settings.sync_status}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={this.props.shouldDisplay(Feature.api_pin_bindings)
|
||||
|| process.env.NODE_ENV !== "production"}>
|
||||
<Row>
|
||||
<Col xs={ColumnWidth.pin}>
|
||||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={ColumnWidth.sequence}>
|
||||
<label>
|
||||
{t("Sequence")}
|
||||
</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<this.currentBindingsList />
|
||||
<this.pinBindingInputGroup />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { Diagnosis, DiagnosisName } from "./connectivity/diagnosis";
|
||||
import { StatusRowProps } from "./connectivity/connectivity_row";
|
||||
import { resetConnectionInfo } from "./actions";
|
||||
import { PinBindings } from "./components/pin_bindings";
|
||||
import { PinBindings } from "./pin_bindings/pin_bindings";
|
||||
import { selectAllDiagnosticDumps } from "../resources/selectors";
|
||||
|
||||
@connect(mapStateToProps)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { sortByNameAndPin } from "../list_and_label_support";
|
||||
|
||||
describe("sortByNameAndPin()", () => {
|
||||
it("sorts", () => {
|
||||
expect(sortByNameAndPin(17, 10)).toEqual(-1); // Button 1 < GPIO 10
|
||||
expect(sortByNameAndPin(2, 10)).toEqual(-1); // GPIO 2 < GPIO 10
|
||||
expect(sortByNameAndPin(17, 23)).toEqual(-1); // Button 1 < Button 2
|
||||
expect(sortByNameAndPin(23, 17)).toEqual(1); // Button 2 > Button 1
|
||||
expect(sortByNameAndPin(1, 1)).toEqual(0); // GPIO 1 == GPIO 1
|
||||
});
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
const mockDevice = {
|
||||
registerGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
unregisterGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
};
|
||||
jest.mock("../../../device", () => ({
|
||||
getDevice: () => (mockDevice)
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
initSave: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { TaggedSequence } from "../../../resources/tagged_resources";
|
||||
import {
|
||||
fakeSequence
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { initSave } from "../../../api/crud";
|
||||
import { PinBindingInputGroupProps, PinBindingType, PinBindingSpecialAction } from "../interfaces";
|
||||
import { PinBindingInputGroup } from "../pin_binding_input_group";
|
||||
import { error, warning } from "farmbot-toastr";
|
||||
|
||||
describe("<PinBindingInputGroup/>", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function fakeProps(): PinBindingInputGroupProps {
|
||||
const fakeResources: TaggedSequence[] = [fakeSequence(), fakeSequence()];
|
||||
fakeResources[0].body.id = 1;
|
||||
fakeResources[0].body.name = "Sequence 1";
|
||||
fakeResources[1].body.id = 2;
|
||||
fakeResources[1].body.name = "Sequence 2";
|
||||
const resources = buildResourceIndex(fakeResources).index;
|
||||
return {
|
||||
pinBindings: [
|
||||
{ pin_number: 10, sequence_id: 1 },
|
||||
{ pin_number: 11, sequence_id: 2 },
|
||||
],
|
||||
dispatch: jest.fn(),
|
||||
resources: resources,
|
||||
shouldDisplay: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toBe(4);
|
||||
});
|
||||
|
||||
it("no pin selected", () => {
|
||||
const wrapper = mount(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
buttons.last().simulate("click");
|
||||
expect(error).toHaveBeenCalledWith("Pin number cannot be blank.");
|
||||
});
|
||||
|
||||
it("no target selected", () => {
|
||||
const wrapper = mount(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
wrapper.setState({ pinNumberInput: 7 });
|
||||
buttons.last().simulate("click");
|
||||
expect(error).toHaveBeenCalledWith("Please select a sequence or action.");
|
||||
});
|
||||
|
||||
it("registers pin: bot", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => x(jest.fn()));
|
||||
const wrapper = mount(<PinBindingInputGroup {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
|
||||
buttons.last().simulate("click");
|
||||
expect(mockDevice.registerGpio).toHaveBeenCalledWith({
|
||||
pin_number: 1, sequence_id: 2
|
||||
});
|
||||
});
|
||||
|
||||
it("registers pin: api", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<PinBindingInputGroup {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
wrapper.setState({ pinNumberInput: 1, sequenceIdInput: 2 });
|
||||
buttons.last().simulate("click");
|
||||
expect(mockDevice.registerGpio).not.toHaveBeenCalled();
|
||||
const expectedResult = expect.objectContaining({
|
||||
kind: "PinBinding",
|
||||
body: {
|
||||
pin_num: 1,
|
||||
sequence_id: 2,
|
||||
binding_type: PinBindingType.standard
|
||||
}
|
||||
});
|
||||
expect(initSave).toHaveBeenCalledWith(expectedResult);
|
||||
});
|
||||
|
||||
it("registers pin: api (special action)", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<PinBindingInputGroup {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.last().text()).toEqual("BIND");
|
||||
wrapper.setState({
|
||||
pinNumberInput: 2,
|
||||
bindingType: PinBindingType.special,
|
||||
sequenceIdInput: undefined,
|
||||
specialActionInput: PinBindingSpecialAction.emergency_lock
|
||||
});
|
||||
buttons.last().simulate("click");
|
||||
expect(mockDevice.registerGpio).not.toHaveBeenCalled();
|
||||
const expectedResult = expect.objectContaining({
|
||||
kind: "PinBinding",
|
||||
body: {
|
||||
pin_num: 2,
|
||||
binding_type: PinBindingType.special,
|
||||
special_action: PinBindingSpecialAction.emergency_lock
|
||||
}
|
||||
});
|
||||
expect(initSave).toHaveBeenCalledWith(expectedResult);
|
||||
});
|
||||
|
||||
it("sets sequence id", () => {
|
||||
const p = fakeProps();
|
||||
const s = p.resources.references[p.resources.byKind.Sequence[0]];
|
||||
const id = s && s.body.id;
|
||||
const wrapper = mount<PinBindingInputGroup>(<PinBindingInputGroup {...p} />);
|
||||
expect(wrapper.instance().state.sequenceIdInput).toEqual(undefined);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.changeSelection({ label: "label", value: id });
|
||||
expect(wrapper.instance().state.sequenceIdInput).toEqual(id);
|
||||
});
|
||||
|
||||
it("sets pin", () => {
|
||||
const wrapper = mount<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.setSelectedPin(10); // pin already bound
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
instance.setSelectedPin(99); // invalid pin
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
instance.setSelectedPin(6); // available pin
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(6);
|
||||
instance.setSelectedPin(1); // reserved pin
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(1);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
"Reserved Raspberry Pi pin may not work as expected.");
|
||||
});
|
||||
|
||||
it("changes pin number", () => {
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
wrapper.find("FBSelect").at(0)
|
||||
.simulate("change", { label: "", value: 7 });
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(7);
|
||||
});
|
||||
|
||||
it("changes binding type", () => {
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
expect(wrapper.instance().state.bindingType).toEqual(PinBindingType.standard);
|
||||
wrapper.find("FBSelect").at(1)
|
||||
.simulate("change", { label: "", value: PinBindingType.special });
|
||||
expect(wrapper.instance().state.bindingType).toEqual(PinBindingType.special);
|
||||
});
|
||||
|
||||
it("changes special action", () => {
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
wrapper.setState({ bindingType: PinBindingType.special });
|
||||
expect(wrapper.instance().state.specialActionInput).toEqual(undefined);
|
||||
wrapper.find("FBSelect").at(2)
|
||||
.simulate("change", { label: "", value: PinBindingSpecialAction.sync });
|
||||
expect(wrapper.instance().state.specialActionInput)
|
||||
.toEqual(PinBindingSpecialAction.sync);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
const mockDevice = {
|
||||
registerGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
unregisterGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
};
|
||||
jest.mock("../../../device", () => ({
|
||||
getDevice: () => (mockDevice)
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
destroy: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import { TaggedSequence } from "../../../resources/tagged_resources";
|
||||
import {
|
||||
fakeSequence, fakePinBinding
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { destroy } from "../../../api/crud";
|
||||
import { PinBindingsList } from "../pin_bindings_list";
|
||||
import { PinBindingsListProps } from "../interfaces";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { sysBtnBindingData } from "../list_and_label_support";
|
||||
|
||||
describe("<PinBindingsList/>", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function fakeProps(): PinBindingsListProps {
|
||||
const fakeResources: TaggedSequence[] = [fakeSequence(), fakeSequence()];
|
||||
fakeResources[0].body.id = 1;
|
||||
fakeResources[0].body.name = "Sequence 1";
|
||||
fakeResources[1].body.id = 2;
|
||||
fakeResources[1].body.name = "Sequence 2";
|
||||
const resources = buildResourceIndex(fakeResources).index;
|
||||
|
||||
return {
|
||||
pinBindings: [
|
||||
{ pin_number: 10, sequence_id: 1 },
|
||||
{ pin_number: 11, sequence_id: 2 },
|
||||
],
|
||||
dispatch: jest.fn(),
|
||||
resources: resources,
|
||||
shouldDisplay: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<PinBindingsList {...fakeProps()} />);
|
||||
["pi gpio 10", "sequence 1", "pi gpio 11", "sequence 2"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it("unregisters pin: bot", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(x => x(jest.fn()));
|
||||
const wrapper = mount(<PinBindingsList {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
buttons.first().simulate("click");
|
||||
expect(mockDevice.unregisterGpio).toHaveBeenCalledWith({
|
||||
pin_number: 10
|
||||
});
|
||||
});
|
||||
|
||||
it("unregisters pin: api", () => {
|
||||
const p = fakeProps();
|
||||
const s = fakeSequence();
|
||||
s.body.id = 1;
|
||||
const b = fakePinBinding();
|
||||
p.resources = buildResourceIndex([b, s]).index;
|
||||
p.shouldDisplay = () => true;
|
||||
p.pinBindings = [{ pin_number: 10, sequence_id: 1, uuid: b.uuid }];
|
||||
const wrapper = mount(<PinBindingsList {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
buttons.first().simulate("click");
|
||||
expect(mockDevice.unregisterGpio).not.toHaveBeenCalled();
|
||||
expect(destroy).toHaveBeenCalledWith(expect.stringContaining("PinBinding"));
|
||||
});
|
||||
|
||||
it("restricts deletion of built-in bindings", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
p.pinBindings = sysBtnBindingData;
|
||||
const wrapper = mount(<PinBindingsList {...p} />);
|
||||
const buttons = wrapper.find("button");
|
||||
buttons.first().simulate("click");
|
||||
expect(mockDevice.unregisterGpio).not.toHaveBeenCalled();
|
||||
expect(destroy).not.toHaveBeenCalled();
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Cannot delete"));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
import * as React from "react";
|
||||
import { PinBindings } from "../pin_bindings";
|
||||
import { mount } from "enzyme";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
fakeSequence, fakePinBinding
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
PinBindingsProps, PinBindingType, PinBindingSpecialAction, SpecialPinBinding
|
||||
} from "../interfaces";
|
||||
|
||||
describe("<PinBindings/>", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function fakeProps(): PinBindingsProps {
|
||||
const fakeSequence1 = fakeSequence();
|
||||
fakeSequence1.body.id = 1;
|
||||
fakeSequence1.body.name = "Sequence 1";
|
||||
const fakeSequence2 = fakeSequence();
|
||||
fakeSequence2.body.id = 2;
|
||||
fakeSequence2.body.name = "Sequence 2";
|
||||
const fakePinBinding1 = fakePinBinding();
|
||||
fakePinBinding1.body.id = 1;
|
||||
fakePinBinding1.body.pin_num = 0;
|
||||
const fakePinBinding2 = fakePinBinding();
|
||||
fakePinBinding2.body.id = 2;
|
||||
fakePinBinding2.body.pin_num = 26;
|
||||
fakePinBinding2.body.binding_type = PinBindingType.special;
|
||||
(fakePinBinding2.body as SpecialPinBinding).special_action =
|
||||
PinBindingSpecialAction.emergency_lock;
|
||||
const resources = buildResourceIndex([
|
||||
fakeSequence1, fakeSequence2, fakePinBinding1, fakePinBinding2
|
||||
]).index;
|
||||
|
||||
bot.hardware.gpio_registry = {
|
||||
10: "1",
|
||||
11: "2"
|
||||
};
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
resources: resources,
|
||||
botToMqttStatus: "up",
|
||||
shouldDisplay: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders: bot", () => {
|
||||
const wrapper = mount(<PinBindings {...fakeProps()} />);
|
||||
["pin bindings", "pin number", "none", "bind"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
["pi gpio 10", "sequence 1", "pi gpio 11", "sequence 2"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toBe(6);
|
||||
});
|
||||
|
||||
it("renders: api", () => {
|
||||
const p = fakeProps();
|
||||
p.shouldDisplay = () => true;
|
||||
const wrapper = mount(<PinBindings {...p} />);
|
||||
["pin bindings", "pin number", "none", "bind"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
["1", "17", "e-stop",
|
||||
"2", "23", "unlock",
|
||||
"26", "action"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toBe(8);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
import { BotState, ShouldDisplay } from "../interfaces";
|
||||
import { NetworkState } from "../../connectivity/interfaces";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
|
||||
export type PinBinding = StandardPinBinding | SpecialPinBinding;
|
||||
|
||||
interface PinBindingBase { id?: number; pin_num: number; }
|
||||
|
||||
export enum PinBindingType {
|
||||
special = "special",
|
||||
standard = "standard",
|
||||
}
|
||||
|
||||
interface StandardPinBinding extends PinBindingBase {
|
||||
binding_type: PinBindingType.standard;
|
||||
sequence_id: number;
|
||||
}
|
||||
|
||||
export interface SpecialPinBinding extends PinBindingBase {
|
||||
binding_type: PinBindingType.special;
|
||||
special_action: PinBindingSpecialAction;
|
||||
}
|
||||
|
||||
export enum PinBindingSpecialAction {
|
||||
emergency_lock = "emergency_lock",
|
||||
emergency_unlock = "emergency_unlock",
|
||||
sync = "sync",
|
||||
reboot = "reboot",
|
||||
power_off = "power_off",
|
||||
dump_info = "dump_info",
|
||||
read_status = "read_status",
|
||||
take_photo = "take_photo",
|
||||
}
|
||||
|
||||
export interface PinBindingsProps {
|
||||
bot: BotState;
|
||||
dispatch: Function;
|
||||
botToMqttStatus: NetworkState;
|
||||
resources: ResourceIndex;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface PinBindingListItems {
|
||||
pin_number: number,
|
||||
sequence_id: number | undefined,
|
||||
special_action?: PinBindingSpecialAction | undefined,
|
||||
binding_type?: PinBindingType,
|
||||
uuid?: string
|
||||
}
|
||||
|
||||
export interface PinBindingsListProps {
|
||||
pinBindings: PinBindingListItems[];
|
||||
resources: ResourceIndex;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export interface PinBindingInputGroupProps {
|
||||
dispatch: Function;
|
||||
resources: ResourceIndex;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
pinBindings: PinBindingListItems[];
|
||||
}
|
||||
|
||||
export interface PinBindingInputGroupState {
|
||||
isEditing: boolean;
|
||||
pinNumberInput: number | undefined;
|
||||
sequenceIdInput: number | undefined;
|
||||
specialActionInput: PinBindingSpecialAction | undefined;
|
||||
bindingType: PinBindingType;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import { t } from "i18next";
|
||||
import { PinBindingType, PinBindingSpecialAction } from "./interfaces";
|
||||
import { DropDownItem } from "../../ui";
|
||||
import { gpio } from "./rpi_gpio_diagram";
|
||||
import { flattenDeep, isNumber } from "lodash";
|
||||
|
||||
export const bindingTypeLabelLookup: { [x: string]: string } = {
|
||||
[PinBindingType.standard]: t("Sequence"),
|
||||
[PinBindingType.special]: t("Action"),
|
||||
"": t("Sequence"),
|
||||
};
|
||||
|
||||
export const specialActionLabelLookup: { [x: string]: string } = {
|
||||
[PinBindingSpecialAction.emergency_lock]: t("E-STOP"),
|
||||
[PinBindingSpecialAction.emergency_unlock]: t("UNLOCK"),
|
||||
[PinBindingSpecialAction.power_off]: t("Shutdown"),
|
||||
[PinBindingSpecialAction.reboot]: t("Reboot"),
|
||||
[PinBindingSpecialAction.sync]: t("Sync"),
|
||||
[PinBindingSpecialAction.dump_info]: t("Diagnostic Report"),
|
||||
[PinBindingSpecialAction.read_status]: t("Read Status"),
|
||||
[PinBindingSpecialAction.take_photo]: t("Take Photo"),
|
||||
"": t("None")
|
||||
};
|
||||
|
||||
export const specialActionList: DropDownItem[] =
|
||||
Object.values(PinBindingSpecialAction)
|
||||
.map((action: PinBindingSpecialAction) =>
|
||||
({ label: specialActionLabelLookup[action], value: action }));
|
||||
|
||||
const sysLedBindings = [5, 12, 13, 16, 20, 22, 24, 25];
|
||||
export const sysBtnBindings = [17, 23];
|
||||
export const sysBindings = sysLedBindings.concat(sysBtnBindings);
|
||||
|
||||
const piI2cPins = [0, 1, 2, 3];
|
||||
export const reservedPiGPIO = piI2cPins;
|
||||
|
||||
const LabeledGpioPins: { [x: number]: string } = {
|
||||
17: "Button 1: E-STOP",
|
||||
23: "Button 2: UNLOCK",
|
||||
27: "Button 3",
|
||||
6: "Button 4",
|
||||
21: "Button 5",
|
||||
};
|
||||
|
||||
export const generatePinLabel = (pin: number) =>
|
||||
LabeledGpioPins[pin]
|
||||
? `${LabeledGpioPins[pin]} (Pi ${pin})`
|
||||
: `Pi GPIO ${pin}`;
|
||||
|
||||
export const validGpioPins: number[] =
|
||||
flattenDeep(gpio)
|
||||
.filter(x => isNumber(x))
|
||||
.map((x: number) => x);
|
||||
// .filter(n => !reservedPiGPIO.includes(n));
|
||||
|
||||
export const sortByNameAndPin = (a: number, b: number) => {
|
||||
const aLabel = generatePinLabel(a).slice(0, 8);
|
||||
const bLabel = generatePinLabel(b).slice(0, 8);
|
||||
// Sort "Button 1", "Button 2", etc.
|
||||
if (aLabel < bLabel) { return -1; }
|
||||
if (aLabel > bLabel) { return 1; }
|
||||
// Sort "GPIO Pi 4", "GPIO Pi 10", etc.
|
||||
if (a < b) { return -1; }
|
||||
if (a > b) { return 1; }
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const RpiPinList = (taken: number[]): DropDownItem[] =>
|
||||
validGpioPins
|
||||
.filter(n => !sysBindings.includes(n))
|
||||
.filter(n => !taken.includes(n))
|
||||
.filter(n => !reservedPiGPIO.includes(n))
|
||||
.sort(sortByNameAndPin)
|
||||
.map(n => ({ label: generatePinLabel(n), value: n }));
|
||||
|
||||
export const sysBtnBindingData = [
|
||||
{
|
||||
pin_number: 17,
|
||||
sequence_id: undefined,
|
||||
special_action: PinBindingSpecialAction.emergency_lock,
|
||||
binding_type: PinBindingType.special,
|
||||
uuid: "FBOS built-in binding: emergency_lock"
|
||||
},
|
||||
{
|
||||
pin_number: 23,
|
||||
sequence_id: undefined,
|
||||
special_action: PinBindingSpecialAction.emergency_unlock,
|
||||
binding_type: PinBindingType.special,
|
||||
uuid: "FBOS built-in binding: emergency_unlock"
|
||||
},
|
||||
];
|
|
@ -0,0 +1,176 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col, DropDownItem, FBSelect, NULL_CHOICE } from "../../ui";
|
||||
import { PinBindingColWidth } from "./pin_bindings";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
import { RpiGpioDiagram } from "./rpi_gpio_diagram";
|
||||
import {
|
||||
PinBindingType, PinBindingSpecialAction,
|
||||
PinBindingInputGroupProps, PinBindingInputGroupState
|
||||
} from "./interfaces";
|
||||
import { isNumber, includes } from "lodash";
|
||||
import { Feature } from "../interfaces";
|
||||
import { initSave } from "../../api/crud";
|
||||
import { taggedPinBinding } from "./tagged_pin_binding_init";
|
||||
import { registerGpioPin } from "../actions";
|
||||
import { error, warning } from "farmbot-toastr";
|
||||
import {
|
||||
validGpioPins, sysBindings, generatePinLabel, RpiPinList,
|
||||
bindingTypeLabelLookup, specialActionLabelLookup, specialActionList, reservedPiGPIO
|
||||
} from "./list_and_label_support";
|
||||
import { SequenceSelectBox } from "../../sequences/sequence_select_box";
|
||||
|
||||
export class PinBindingInputGroup
|
||||
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
|
||||
state = {
|
||||
isEditing: false,
|
||||
pinNumberInput: undefined,
|
||||
sequenceIdInput: undefined,
|
||||
specialActionInput: undefined,
|
||||
bindingType: PinBindingType.standard,
|
||||
};
|
||||
|
||||
changeSelection = (input: DropDownItem) => {
|
||||
this.setState({ sequenceIdInput: parseInt("" + input.value) });
|
||||
}
|
||||
|
||||
setSelectedPin = (pin: number | undefined) => {
|
||||
if (!includes(this.boundPins, pin)) {
|
||||
if (includes(validGpioPins, pin)) {
|
||||
this.setState({ pinNumberInput: pin });
|
||||
if (includes(reservedPiGPIO, pin)) {
|
||||
warning(t("Reserved Raspberry Pi pin may not work as expected."));
|
||||
}
|
||||
} else {
|
||||
error(t("Invalid Raspberry Pi GPIO pin number."));
|
||||
}
|
||||
} else {
|
||||
error(t("Raspberry Pi GPIO pin already bound."));
|
||||
}
|
||||
}
|
||||
|
||||
get boundPins(): number[] | undefined {
|
||||
const userBindings = this.props.pinBindings.map(x => x.pin_number);
|
||||
return userBindings.concat(sysBindings);
|
||||
}
|
||||
|
||||
bindPin = () => {
|
||||
const { shouldDisplay, dispatch } = this.props;
|
||||
const {
|
||||
pinNumberInput, sequenceIdInput, bindingType, specialActionInput
|
||||
} = this.state;
|
||||
if (isNumber(pinNumberInput)) {
|
||||
if (bindingType && (sequenceIdInput || specialActionInput)) {
|
||||
if (shouldDisplay(Feature.api_pin_bindings)) {
|
||||
dispatch(initSave(
|
||||
bindingType == PinBindingType.special
|
||||
? taggedPinBinding({
|
||||
pin_num: pinNumberInput,
|
||||
special_action: specialActionInput,
|
||||
binding_type: bindingType
|
||||
})
|
||||
: taggedPinBinding({
|
||||
pin_num: pinNumberInput,
|
||||
sequence_id: sequenceIdInput,
|
||||
binding_type: bindingType
|
||||
})));
|
||||
} else {
|
||||
dispatch(registerGpioPin({
|
||||
pin_number: pinNumberInput,
|
||||
sequence_id: sequenceIdInput || 0
|
||||
}));
|
||||
}
|
||||
this.setState({
|
||||
pinNumberInput: undefined,
|
||||
sequenceIdInput: undefined,
|
||||
specialActionInput: undefined,
|
||||
bindingType: PinBindingType.standard,
|
||||
});
|
||||
} else {
|
||||
error(t("Please select a sequence or action."));
|
||||
}
|
||||
} else {
|
||||
error(t("Pin number cannot be blank."));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
pinNumberInput, sequenceIdInput, bindingType, specialActionInput
|
||||
} = this.state;
|
||||
const { shouldDisplay } = this.props;
|
||||
|
||||
return <Row>
|
||||
<Col xs={PinBindingColWidth.pin}>
|
||||
<Row>
|
||||
<Col xs={1}>
|
||||
<Popover position={Position.TOP}>
|
||||
<i className="fa fa-th-large" />
|
||||
<RpiGpioDiagram
|
||||
boundPins={this.boundPins}
|
||||
setSelectedPin={this.setSelectedPin}
|
||||
selectedPin={this.state.pinNumberInput} />
|
||||
</Popover>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<FBSelect
|
||||
key={"pin_number_input_" + pinNumberInput}
|
||||
onChange={ddi =>
|
||||
this.setSelectedPin(parseInt("" + ddi.value))}
|
||||
selectedItem={isNumber(pinNumberInput)
|
||||
? {
|
||||
label: generatePinLabel(pinNumberInput),
|
||||
value: "" + pinNumberInput
|
||||
}
|
||||
: NULL_CHOICE}
|
||||
list={RpiPinList(this.boundPins || [])} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.type}>
|
||||
<FBSelect
|
||||
key={"binding_type_input_" + pinNumberInput}
|
||||
onChange={(ddi: { label: string, value: PinBindingType }) =>
|
||||
this.setState({ bindingType: ddi.value })}
|
||||
selectedItem={{
|
||||
label: bindingTypeLabelLookup[bindingType],
|
||||
value: bindingType
|
||||
}}
|
||||
list={Object.entries(bindingTypeLabelLookup)
|
||||
.filter(([value, _]) => !(value == ""))
|
||||
.filter(([value, _]) =>
|
||||
shouldDisplay(Feature.api_pin_bindings)
|
||||
|| !(value == PinBindingType.special))
|
||||
.map(([value, label]) => ({ label, value }))} />
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.target}>
|
||||
{bindingType == PinBindingType.special
|
||||
? <FBSelect
|
||||
key={"special_action_input_" + pinNumberInput}
|
||||
onChange={
|
||||
(ddi: { label: string, value: PinBindingSpecialAction }) =>
|
||||
this.setState({ specialActionInput: ddi.value })}
|
||||
selectedItem={specialActionInput
|
||||
? {
|
||||
label: specialActionLabelLookup[specialActionInput || ""],
|
||||
value: "" + specialActionInput
|
||||
}
|
||||
: NULL_CHOICE}
|
||||
list={specialActionList} />
|
||||
: <SequenceSelectBox
|
||||
key={sequenceIdInput}
|
||||
onChange={this.changeSelection}
|
||||
resources={this.props.resources}
|
||||
sequenceId={sequenceIdInput} />}
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.button}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
type="button"
|
||||
onClick={() => { this.bindPin(); }} >
|
||||
{t("BIND")}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
Widget, WidgetBody, WidgetHeader,
|
||||
Row, Col,
|
||||
} from "../../ui/index";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { Feature } from "../interfaces";
|
||||
import { selectAllPinBindings } from "../../resources/selectors";
|
||||
import { MustBeOnline } from "../must_be_online";
|
||||
import {
|
||||
PinBinding, PinBindingSpecialAction, PinBindingType, PinBindingsProps,
|
||||
PinBindingListItems
|
||||
} from "./interfaces";
|
||||
import { sysBtnBindingData } from "./list_and_label_support";
|
||||
import { PinBindingsList } from "./pin_bindings_list";
|
||||
import { PinBindingInputGroup } from "./pin_binding_input_group";
|
||||
|
||||
export enum PinBindingColWidth {
|
||||
pin = 4,
|
||||
type = 3,
|
||||
target = 4,
|
||||
button = 1
|
||||
}
|
||||
|
||||
const getBindingTarget = (bindingBody: PinBinding): {
|
||||
sequence_id: number | undefined,
|
||||
special_action: PinBindingSpecialAction | undefined
|
||||
} => {
|
||||
return bindingBody.binding_type == PinBindingType.special
|
||||
? { sequence_id: undefined, special_action: bindingBody.special_action }
|
||||
: { sequence_id: bindingBody.sequence_id, special_action: undefined };
|
||||
};
|
||||
|
||||
export const PinBindings = (props: PinBindingsProps) => {
|
||||
const { dispatch, resources, shouldDisplay, botToMqttStatus, bot } = props;
|
||||
|
||||
const getPinBindings = (): PinBindingListItems[] => {
|
||||
if (shouldDisplay(Feature.api_pin_bindings)) {
|
||||
const userBindings = selectAllPinBindings(resources)
|
||||
.map(binding => {
|
||||
const { uuid, body } = binding;
|
||||
const sequence_id = getBindingTarget(body).sequence_id;
|
||||
const special_action = getBindingTarget(body).special_action;
|
||||
return {
|
||||
pin_number: body.pin_num,
|
||||
sequence_id,
|
||||
special_action,
|
||||
binding_type: body.binding_type,
|
||||
uuid: uuid
|
||||
};
|
||||
});
|
||||
return userBindings.concat(sysBtnBindingData);
|
||||
} else {
|
||||
const { gpio_registry } = props.bot.hardware;
|
||||
return Object.entries(gpio_registry || {})
|
||||
.map(([pin_number, sequence_id]) => {
|
||||
return {
|
||||
pin_number: parseInt(pin_number),
|
||||
sequence_id: parseInt(sequence_id || "")
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return <Widget className="pin-bindings-widget">
|
||||
<WidgetHeader
|
||||
title={t("Pin Bindings")}
|
||||
helpText={ToolTips.PIN_BINDINGS} />
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
syncStatus={bot.hardware.informational_settings.sync_status}
|
||||
networkState={botToMqttStatus}
|
||||
lockOpen={shouldDisplay(Feature.api_pin_bindings)
|
||||
|| process.env.NODE_ENV !== "production"}>
|
||||
<Row>
|
||||
<Col xs={PinBindingColWidth.pin}>
|
||||
<label>
|
||||
{t("Pin Number")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.type}>
|
||||
<label>
|
||||
{t("Binding")}
|
||||
</label>
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.target}>
|
||||
<label>
|
||||
{t("target")}
|
||||
</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<PinBindingsList
|
||||
pinBindings={getPinBindings()}
|
||||
dispatch={dispatch}
|
||||
resources={resources}
|
||||
shouldDisplay={shouldDisplay} />
|
||||
<PinBindingInputGroup
|
||||
pinBindings={getPinBindings()}
|
||||
dispatch={dispatch}
|
||||
resources={resources}
|
||||
shouldDisplay={shouldDisplay} />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Feature } from "../interfaces";
|
||||
import {
|
||||
sysBtnBindings, bindingTypeLabelLookup, specialActionLabelLookup,
|
||||
generatePinLabel, sortByNameAndPin
|
||||
} from "./list_and_label_support";
|
||||
import { destroy } from "../../api/crud";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { findSequenceById } from "../../resources/selectors";
|
||||
import { unregisterGpioPin } from "../actions";
|
||||
import { PinBindingColWidth } from "./pin_bindings";
|
||||
import { PinBindingsListProps } from "./interfaces";
|
||||
|
||||
export const PinBindingsList = (props: PinBindingsListProps) => {
|
||||
const { pinBindings, resources, shouldDisplay, dispatch } = props;
|
||||
|
||||
const deleteBinding = (pin: number, uuid?: string) => {
|
||||
if (shouldDisplay(Feature.api_pin_bindings)) {
|
||||
if (!sysBtnBindings.includes(pin)) {
|
||||
dispatch(destroy(uuid || ""));
|
||||
} else {
|
||||
error(t("Cannot delete built-in pin binding."));
|
||||
}
|
||||
} else {
|
||||
dispatch(unregisterGpioPin(pin));
|
||||
}
|
||||
};
|
||||
|
||||
const delBtnColor = (pin: number) =>
|
||||
sysBtnBindings.includes(pin) ? "pseudo-disabled" : "red";
|
||||
|
||||
return <div className={"bindings-list"}>
|
||||
{pinBindings
|
||||
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
|
||||
.map(x => {
|
||||
const { pin_number, sequence_id, binding_type, special_action } = x;
|
||||
return <Row key={`pin_${pin_number}_binding`}>
|
||||
<Col xs={PinBindingColWidth.pin}>
|
||||
{generatePinLabel(pin_number)}
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.type}>
|
||||
{t(bindingTypeLabelLookup[binding_type || ""])}
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.target}>
|
||||
{sequence_id
|
||||
? findSequenceById(resources, sequence_id).body.name
|
||||
: t(specialActionLabelLookup[special_action || ""])}
|
||||
</Col>
|
||||
<Col xs={PinBindingColWidth.button}>
|
||||
<button
|
||||
className={`fb-button ${delBtnColor(pin_number)}`}
|
||||
onClick={() => deleteBinding(pin_number, x.uuid)}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</Col>
|
||||
</Row>;
|
||||
})}
|
||||
</div>;
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { Color } from "../../ui/colors";
|
||||
import * as _ from "lodash";
|
||||
import { reservedPiGPIO } from "./list_and_label_support";
|
||||
|
||||
export interface RpiGpioDiagramProps {
|
||||
boundPins: number[] | undefined;
|
||||
|
@ -55,7 +56,7 @@ export class RpiGpioDiagram extends React.Component<RpiGpioDiagramProps, RpiGpio
|
|||
{[3, 5.5].map((x, xi) => {
|
||||
return _.range(8, 56, 2.5).map((y, yi) => {
|
||||
const pin = gpio[yi][xi];
|
||||
const color = () => {
|
||||
const normalColor = () => {
|
||||
switch (pin) {
|
||||
case "GND":
|
||||
return Color.black;
|
||||
|
@ -70,9 +71,12 @@ export class RpiGpioDiagram extends React.Component<RpiGpioDiagramProps, RpiGpio
|
|||
return Color.green;
|
||||
}
|
||||
};
|
||||
const color = _.isNumber(pin) && reservedPiGPIO.includes(pin)
|
||||
? Color.magenta
|
||||
: normalColor();
|
||||
const pinColor = _.includes(this.props.boundPins, pin)
|
||||
? Color.darkGray
|
||||
: color();
|
||||
: color;
|
||||
return <rect strokeWidth={0.5} key={`gpio_${pin}_${xi}_${yi}`}
|
||||
stroke={pinColor} fill={pinColor}
|
||||
x={x} y={y} width={1.5} height={1.5}
|
|
@ -0,0 +1,31 @@
|
|||
import { PinBindingType, PinBindingSpecialAction, PinBinding } from "./interfaces";
|
||||
import { TaggedPinBinding, SpecialStatus } from "../../resources/tagged_resources";
|
||||
|
||||
export const taggedPinBinding =
|
||||
(bodyInputs: {
|
||||
pin_num: number,
|
||||
binding_type: PinBindingType,
|
||||
sequence_id?: number | undefined,
|
||||
special_action?: PinBindingSpecialAction | undefined
|
||||
}): TaggedPinBinding => {
|
||||
const { pin_num, binding_type, special_action, sequence_id } = bodyInputs;
|
||||
const body: PinBinding =
|
||||
binding_type == PinBindingType.special
|
||||
? {
|
||||
pin_num,
|
||||
binding_type,
|
||||
special_action: special_action
|
||||
|| PinBindingSpecialAction.emergency_lock,
|
||||
}
|
||||
: {
|
||||
pin_num,
|
||||
binding_type,
|
||||
sequence_id: sequence_id || 0,
|
||||
};
|
||||
return {
|
||||
uuid: "WILL_BE_CHANGED_BY_REDUCER",
|
||||
specialStatus: SpecialStatus.SAVED,
|
||||
kind: "PinBinding",
|
||||
body
|
||||
};
|
||||
};
|
|
@ -79,10 +79,12 @@ describe("<FarmwarePage />", () => {
|
|||
);
|
||||
it("renders installed Farmware page", () => {
|
||||
const p = fakeProps();
|
||||
p.farmwares["My Fake Farmware"] = fakeFarmware();
|
||||
p.currentFarmware = "My Fake Farmware";
|
||||
const farmware = fakeFarmware();
|
||||
farmware.name = "My Fake Test Farmware";
|
||||
p.farmwares["My Fake Test Farmware"] = farmware;
|
||||
p.currentFarmware = "My Fake Test Farmware";
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
["My Fake Farmware", "Does things", "Run", "Config 1",
|
||||
["My Fake Test Farmware", "Does things", "Run", "Config 1",
|
||||
"Information", "Description", "Version", "Update", "Remove"
|
||||
].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
|
@ -92,8 +94,9 @@ describe("<FarmwarePage />", () => {
|
|||
const p = fakeProps();
|
||||
const farmware = fakeFarmware();
|
||||
farmware.config = [];
|
||||
p.farmwares["My Fake Farmware"] = farmware;
|
||||
p.currentFarmware = "My Fake Farmware";
|
||||
farmware.name = "My Fake Test Farmware";
|
||||
p.farmwares["My Fake Test Farmware"] = farmware;
|
||||
p.currentFarmware = "My Fake Test Farmware";
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
["My Fake Farmware", "Does things", "Run", "No inputs provided."
|
||||
].map(string =>
|
||||
|
@ -104,10 +107,11 @@ describe("<FarmwarePage />", () => {
|
|||
const p = fakeProps();
|
||||
const farmware = fakeFarmware();
|
||||
farmware.config = [];
|
||||
p.farmwares["My Fake Farmware"] = farmware;
|
||||
p.currentFarmware = "My Fake Farmware";
|
||||
farmware.name = "My Fake Test Farmware";
|
||||
p.farmwares["My Fake Test Farmware"] = farmware;
|
||||
p.currentFarmware = "My Fake Test Farmware";
|
||||
const wrapper = mount(<FarmwarePage {...p} />);
|
||||
clickButton(wrapper, 1, "Run");
|
||||
expect(mockDevice.execScript).toHaveBeenCalledWith("My Fake Farmware");
|
||||
expect(mockDevice.execScript).toHaveBeenCalledWith("My Fake Test Farmware");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,20 +11,6 @@ import { ChannelName } from "./sequences/interfaces";
|
|||
in the UI. Only certain colors are valid. */
|
||||
export type Color = FarmBotJsColor;
|
||||
|
||||
export type PinBinding = StandardPinBinding | SpecialPinBinding;
|
||||
|
||||
interface PinBindingBase { id?: number; pin_num: number; }
|
||||
|
||||
interface StandardPinBinding extends PinBindingBase {
|
||||
binding_type: "standard";
|
||||
sequence_id: number;
|
||||
}
|
||||
|
||||
export interface SpecialPinBinding extends PinBindingBase {
|
||||
binding_type: "special";
|
||||
special_action: string; // TODO: Maybe use enum? RC 15 JUL 18
|
||||
}
|
||||
|
||||
export interface Sensor {
|
||||
id?: number;
|
||||
pin: number | undefined;
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
SensorReading,
|
||||
Sensor,
|
||||
DeviceConfig,
|
||||
PinBinding
|
||||
} from "../interfaces";
|
||||
import { Peripheral } from "../controls/peripherals/interfaces";
|
||||
import { User } from "../auth/interfaces";
|
||||
|
@ -25,6 +24,7 @@ import { FirmwareConfig } from "../config_storage/firmware_configs";
|
|||
import { WebAppConfig } from "../config_storage/web_app_configs";
|
||||
import { FarmwareInstallation } from "../farmware/interfaces";
|
||||
import { assertUuid } from "./util";
|
||||
import { PinBinding } from "../devices/pin_bindings/interfaces";
|
||||
|
||||
export type ResourceName =
|
||||
| "Crop"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import axios from "axios";
|
||||
import { Log, Point, SensorReading, Sensor, DeviceConfig, PinBinding } from "../interfaces";
|
||||
import { Log, Point, SensorReading, Sensor, DeviceConfig } from "../interfaces";
|
||||
import { API } from "../api";
|
||||
import { Sequence } from "../sequences/interfaces";
|
||||
import { Tool } from "../tools/interfaces";
|
||||
|
@ -16,6 +16,7 @@ import { Session } from "../session";
|
|||
import { FbosConfig } from "../config_storage/fbos_configs";
|
||||
import { FarmwareInstallation } from "../farmware/interfaces";
|
||||
import { FirmwareConfig } from "../config_storage/firmware_configs";
|
||||
import { PinBinding } from "../devices/pin_bindings/interfaces";
|
||||
|
||||
export interface ResourceReadyPayl {
|
||||
name: ResourceName;
|
||||
|
|
|
@ -16,4 +16,5 @@ export enum Color {
|
|||
black = "#000000",
|
||||
orange = "#ffa500",
|
||||
blue = "#3377dd",
|
||||
magenta = "#a64d79",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue