Skip to content

Commit

Permalink
Add a widget for ProfiledPIDController (#764)
Browse files Browse the repository at this point in the history
Effectively identical to PIDController for now, but future updates to WPILib will send more information
  • Loading branch information
SamCarlberg authored Feb 17, 2023
1 parent bb5c1b6 commit 4edd2f5
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import edu.wpi.first.shuffleboard.plugin.base.data.types.MecanumDriveType;
import edu.wpi.first.shuffleboard.plugin.base.data.types.PIDCommandType;
import edu.wpi.first.shuffleboard.plugin.base.data.types.PIDControllerType;
import edu.wpi.first.shuffleboard.plugin.base.data.types.ProfiledPIDControllerType;
import edu.wpi.first.shuffleboard.plugin.base.data.types.PowerDistributionType;
import edu.wpi.first.shuffleboard.plugin.base.data.types.QuadratureEncoderType;
import edu.wpi.first.shuffleboard.plugin.base.data.types.RelayType;
Expand Down Expand Up @@ -57,6 +58,7 @@
import edu.wpi.first.shuffleboard.plugin.base.widget.NumberSliderWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.PIDCommandWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.PIDControllerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.ProfiledPIDControllerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.PowerDistributionPanelWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.RelayWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.RobotPreferencesWidget;
Expand All @@ -79,7 +81,7 @@
@Description(
group = "edu.wpi.first.shuffleboard",
name = "Base",
version = "1.3.4",
version = "1.3.5",
summary = "Defines all the WPILib data types and stock widgets"
)
@SuppressWarnings("PMD.CouplingBetweenObjects")
Expand Down Expand Up @@ -118,6 +120,7 @@ public List<DataType> getDataTypes() {
CommandType.Instance,
PIDCommandType.Instance,
PIDControllerType.Instance,
ProfiledPIDControllerType.Instance,
AccelerometerType.Instance,
ThreeAxisAccelerometerType.Instance,
GyroType.Instance,
Expand Down Expand Up @@ -154,6 +157,7 @@ public List<ComponentType> getComponents() {
WidgetType.forAnnotatedWidget(AccelerometerWidget.class),
WidgetType.forAnnotatedWidget(ThreeAxisAccelerometerWidget.class),
WidgetType.forAnnotatedWidget(PIDControllerWidget.class),
WidgetType.forAnnotatedWidget(ProfiledPIDControllerWidget.class),
WidgetType.forAnnotatedWidget(GyroWidget.class),
WidgetType.forAnnotatedWidget(RelayWidget.class),
WidgetType.forAnnotatedWidget(DifferentialDriveWidget.class),
Expand Down Expand Up @@ -183,6 +187,7 @@ public Map<DataType, ComponentType> getDefaultComponents() {
.put(CommandType.Instance, WidgetType.forAnnotatedWidget(CommandWidget.class))
.put(PIDCommandType.Instance, WidgetType.forAnnotatedWidget(PIDCommandWidget.class))
.put(PIDControllerType.Instance, WidgetType.forAnnotatedWidget(PIDControllerWidget.class))
.put(ProfiledPIDControllerType.Instance, WidgetType.forAnnotatedWidget(ProfiledPIDControllerWidget.class))
.put(AccelerometerType.Instance, WidgetType.forAnnotatedWidget(AccelerometerWidget.class))
.put(ThreeAxisAccelerometerType.Instance, WidgetType.forAnnotatedWidget(ThreeAxisAccelerometerWidget.class))
.put(GyroType.Instance, WidgetType.forAnnotatedWidget(GyroWidget.class))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package edu.wpi.first.shuffleboard.plugin.base.data;

import com.google.common.collect.ImmutableMap;
import edu.wpi.first.shuffleboard.api.data.ComplexData;

import java.util.Map;
import java.util.Objects;

/**
* Data for a profiled PID controller from WPILib. WPILib currently sends P, I, D, and the goal position; this makes
* it effectively identical to {@link PIDControllerData}. Future updates to WPILib to fully support profiled PID
* controllers will result in extra fields in the data; for example, the goal velocity or trapezoidal constraints.
*/
public final class ProfiledPIDControllerData extends ComplexData<ProfiledPIDControllerData> {

private final double p;
private final double i;
private final double d;
private final double goal;

/**
* Creates a new PIDController data object.
*
* @param p the proportional constant
* @param i the integral constant
* @param d the derivative constant
* @param goal the controller goal
*/
public ProfiledPIDControllerData(double p, double i, double d, double goal) {
this.p = p;
this.i = i;
this.d = d;
this.goal = goal;
}

/**
* Creates a new data object from a map. The map should contain values for all the properties of the data object. If
* a value is missing, the default value of {@code 0} (for numbers) is used.
*/
public ProfiledPIDControllerData(Map<String, Object> map) {
this((double) map.getOrDefault("p", 0.0),
(double) map.getOrDefault("i", 0.0),
(double) map.getOrDefault("d", 0.0),
(double) map.getOrDefault("goal", 0.0));
}

public double getP() {
return p;
}

public double getI() {
return i;
}

public double getD() {
return d;
}

public double getGoal() {
return goal;
}


public ProfiledPIDControllerData withP(double p) {
return new ProfiledPIDControllerData(p, i, d, goal);
}

public ProfiledPIDControllerData withI(double i) {
return new ProfiledPIDControllerData(p, i, d, goal);
}

public ProfiledPIDControllerData withD(double d) {
return new ProfiledPIDControllerData(p, i, d, goal);
}

public ProfiledPIDControllerData withGoal(double goal) {
return new ProfiledPIDControllerData(p, i, d, goal);
}

@Override
public Map<String, Object> asMap() {
return ImmutableMap.<String, Object>builder()
.put("p", p)
.put("i", i)
.put("d", d)
.put("goal", goal)
.build();
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ProfiledPIDControllerData that = (ProfiledPIDControllerData) obj;
return this.p == that.p
&& this.i == that.i
&& this.d == that.d
&& this.goal == that.goal;
}

@Override
public int hashCode() {
return Objects.hash(p, i, d, goal);
}

@Override
public String toString() {
return String.format("ProfiledPIDControllerData(p=%s, i=%s, d=%s, goal=%s)",
p, i, d, goal);
}

@Override
public String toHumanReadableString() {
return String.format("p=%.3f, i=%.3f, d=%.3f, goal=%.3f", p, i, d, goal);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package edu.wpi.first.shuffleboard.plugin.base.data.types;

import edu.wpi.first.shuffleboard.api.data.ComplexDataType;
import edu.wpi.first.shuffleboard.plugin.base.data.ProfiledPIDControllerData;

import java.util.Map;
import java.util.function.Function;

public final class ProfiledPIDControllerType extends ComplexDataType<ProfiledPIDControllerData> {

public static final ProfiledPIDControllerType Instance = new ProfiledPIDControllerType();

private ProfiledPIDControllerType() {
super("ProfiledPIDController", ProfiledPIDControllerData.class);
}

@Override
public Function<Map<String, Object>, ProfiledPIDControllerData> fromMap() {
return ProfiledPIDControllerData::new;
}

@Override
public ProfiledPIDControllerData getDefaultValue() {
return new ProfiledPIDControllerData(0, 0, 0, 0);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package edu.wpi.first.shuffleboard.plugin.base.widget;

import edu.wpi.first.shuffleboard.api.components.NumberField;
import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import edu.wpi.first.shuffleboard.plugin.base.data.ProfiledPIDControllerData;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;

/**
* A widget for controlling Profiled PID controllers. This gives control over the three PID constants and the controller
* position goal.
*
* <p>Future updates to the sendable implementation for profiled PID controllers (such as support for goal velocity and
* trapezoidal constraints) will result in more available controls in this widget.
*/
@Description(name = "Profiled PID Controller", dataTypes = ProfiledPIDControllerData.class)
@ParametrizedController("ProfiledPIDControllerWidget.fxml")
public class ProfiledPIDControllerWidget extends SimpleAnnotatedWidget<ProfiledPIDControllerData> {

@FXML
private Pane root;
@FXML
private NumberField pField;
@FXML
private NumberField iField;
@FXML
private NumberField dField;
@FXML
private NumberField goalField;

@FXML
private void initialize() {
root.setStyle("-fx-font-size: 10pt;");
dataProperty().addListener((__, old, newData) -> {
pField.setNumber(newData.getP());
iField.setNumber(newData.getI());
dField.setNumber(newData.getD());
goalField.setNumber(newData.getGoal());
});

actOnFocusLost(pField);
actOnFocusLost(iField);
actOnFocusLost(dField);
actOnFocusLost(goalField);
}

private void actOnFocusLost(TextField field) {
field.focusedProperty().addListener((__, was, is) -> {
if (!is) {
field.getOnAction().handle(new ActionEvent(this, field));
}
});
}

@Override
public Pane getView() {
return root;
}

@FXML
private void setP() {
setData(getData().withP(pField.getNumber()));
}

@FXML
private void setI() {
setData(getData().withI(iField.getNumber()));
}

@FXML
private void setD() {
setData(getData().withD(dField.getNumber()));
}

@FXML
private void setGoal() {
setData(getData().withGoal(goalField.getNumber()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import edu.wpi.first.shuffleboard.api.components.NumberField?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="edu.wpi.first.shuffleboard.plugin.base.widget.ProfiledPIDControllerWidget"
fx:id="root"
minWidth="112" minHeight="168"
hgap="4" vgap="4">
<columnConstraints>
<ColumnConstraints minWidth="52" prefWidth="52" maxWidth="52"/>
<ColumnConstraints maxWidth="Infinity"/>
</columnConstraints>
<padding>
<Insets topRightBottomLeft="8"/>
</padding>
<Label text="P" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
<Label text="I" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<Label text="D" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<Label text="Goal" GridPane.columnIndex="0" GridPane.rowIndex="3"/>
<NumberField fx:id="pField" onAction="#setP" GridPane.columnIndex="1" GridPane.rowIndex="0" GridPane.hgrow="ALWAYS"/>
<NumberField fx:id="iField" onAction="#setI" GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.hgrow="ALWAYS"/>
<NumberField fx:id="dField" onAction="#setD" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.hgrow="ALWAYS"/>
<NumberField fx:id="goalField" onAction="#setGoal" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.hgrow="ALWAYS"/>
</GridPane>

0 comments on commit 4edd2f5

Please sign in to comment.