Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into resource_service

pull/915/head
Rick Carlino 2018-07-18 10:15:31 -05:00
commit 9582c0a590
23 changed files with 936 additions and 444 deletions

View File

@ -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,
});
}

View File

@ -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()
}));

View File

@ -302,6 +302,7 @@ a {
}
.bindings-list {
margin-bottom: 1rem;
font-size: 1.2rem;
}
}

View File

@ -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);
});
});

View File

@ -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>;
}
}

View File

@ -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)

View File

@ -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
});
});

View File

@ -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);
});
});

View File

@ -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"));
});
});

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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"
},
];

View File

@ -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>;
}
}

View File

@ -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>;
};

View File

@ -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>;
};

View File

@ -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}

View File

@ -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
};
};

View File

@ -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");
});
});

View File

@ -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;

View File

@ -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"

View File

@ -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;

View File

@ -16,4 +16,5 @@ export enum Color {
black = "#000000",
orange = "#ffa500",
blue = "#3377dd",
magenta = "#a64d79",
}