Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fluent builders for more flexibly adding data to Shuffleboard #1022

Merged
merged 47 commits into from
Sep 28, 2018

Conversation

SamCarlberg
Copy link
Member

@SamCarlberg SamCarlberg commented May 4, 2018

Overview

Motivation

A big question people have had about Shuffleboard (and, to an extent, SmartDashboard) is how to configure the dashboard from their robot code. Specifically, how to use something like SmartDashboard.getX without having to also remember to add a call to SmartDashboard.putX at the beginning of the program.

Unfortunately, simply using SmartDashboard.getX doesn't work without doing some legwork ahead of time without introducing a custom compile task to scan for those method calls - this would not be a worthwhile solution. A solution that could be done in the current version of WPILib, or with just minor tweaks, would to be to use NetworkTableEntry instances for these variables; for example:

// Current approach with WPILib
void useX() {
  double x = SmartDashboard.getNumber("X");
}
// With NetworkTableEntry approach
public static final NetworkTableEntry xEntry = SmartDashboard.getEntry("X");

void useX() {
  double x = xEntry.getDouble(defaultValue);
}

An advantage to this method is that typos are eliminated; there's only one instance of the key string, and a typo made to the variable name would cause a compiler error. However, this still limits how data gets displayed in Shuffleboard; everything is still in the "Smartdashboard" tab.

Using the simple entry approach would fail for defining a Shuffleboard configuration. Users should not have to keep track of the names of the tab or layouts that contain the data they really want to read or update. It also doesn't address the issue of how the configuration should be sent to the dashboard.

Setup of the builders

The builders do two things: makes it easy to define where and how a piece of data is displayed or controlled, and gives a handle to a NetworkTableEntry for that data, when applicable (Sendables do not get entry handles, since they update and read their own tables). Widgets and layouts also have their properties configurable via the builders. For ease of use, whitespace and capitalization are ignored so users don't have to know the actual property names, which are not what gets displayed in the property editor dialogs in the dashboard.

Data, tab, and layout names are prohibited from containing forward slashes /; typically, these are used for defining subtables under the base SmartDashboard table. This is unnecessary in Shuffleboard, since these values can now be placed in specific tabs or layouts.

Internally, the Shuffleboard class will handle specifying the configuration in NetworkTables for the NetworkTables plugin to read on the client side.

Adding an entry

NetworkTableEntry myEntry = Shuffleboard.getTab(tabName)
  .getLayout(layoutType, layoutTitle) // optional
  .withProperties(layoutProperties)   // optional (requires layout to be set)
  .add(entryName, initialValue)       // REQUIRED
  .toWidget(widgetType)               // optional
  .withProperties(widgetProperties)   // optional
  .getEntry();

Adding a sendable

Shuffleboard.getTab(tabName)
  .getLayout(layoutType, layoutTitle) // optional
  .withProperties(layoutProperties)   // optional (requires layout to be set)
  .add(title, sendable)               // REQUIRED (name can be omitted if the sendable's name has been set)
  .toWidget(widgetType)               // optional
  .withProperties(widgetProperties)   // optional

The two builders look identical, except that the builder for entries has a terminal getEntry() function. This can be called at any point during the chain, but must be called in order to get a handle to the NetworkTableEntry being added. A call to Shuffleboard.update after completing a builder is required to set up the configuration metadata (tab name, layout info, widget info, etc), as well as update sendables.

Keeping variables around

ShuffleboardTab myTab = Shuffleboard.getTab(tabName);
ShuffleboardLayout myLayout = myTab.getLayout(layoutType, layoutTitle);
myLayout.add(name, value);
myTab.add(name, value);

Example use

Adding a slider to a tab named "Drivers Tab" to control the maximum speed of the drivetrain

RobotDrive driveBase = ...
NetworkTableEntry maxSpeed = Shuffleboard.getTab("Drivers")
  .add("Max Speed", 1)
  .withWidget("Number Slider")
  .withProperties(Map.of("min", 0.0, "max", 1.0)) // Note: Map.of introduced in Java 9
  .getEntry();
...

driveBase.setMaxOutput(maxSpeed.getDouble(1.0));

Still to do

  • C++ version
  • Add support in Shuffleboard (this is important)

Waiting on

  • WPILib migration to Java 9+; specifically, for the addition of Map.of for easily setting properties

Possible feature improvements

  • Allow default values to be specified for entries Added
  • Allow for nested layouts Added
  • Allow for adding NetworkTableEntry instances directly i.e. myTab.add(myEntry) Not feasible
  • More documentation to base classes and interfaces.
  • Specifying exact layout properties (tab's grid size, tile's position and size in the grid) Added

@SamCarlberg SamCarlberg added the wip label May 4, 2018
@Override
public void teleopPeriodic() {
Shuffleboard.update();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add this example to the examples.xml (actually by the time this merges I hope to have replaced that with the json equivalent). Also, these updates should be in robotPeriodic, not the individual periodic methods. robotPeriodic was specificially added for purposes like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll wait on the VSCode JSON stuff before merging.

I see the point of using robotPeriodic for consistency, but I do like specifying both autonomous and teleop periodic functions because those are what users will be using in their real programs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I would worry about teams forgetting about it in test mode and wondering why its not running. robotPeriodic is specifically for things that should always be updated every loop. I've been trying to suggest it more to teams when helping for dashboard things. I'm actually debating adding it to the default project as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, cool. In that case, I'll definitely move it over

// Update sendables
// Basic entries do not need to be updated by us; they are set by users via the
// NetworkTableEntry instances they get when completing an add() operation
m_sendables.forEach(data -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this allocate a lambda per call? Why not just a standard loop? Since this is called once per robot loop, we should avoid allocations as much as possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a non-capturing lambda, so there's only one object allocated.

NetworkTable baseTable = getBaseTable();
// Update tabs
NetworkTable tabsTable = baseTable.getSubTable(".tabs");
m_builders.stream()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about the allocations here, but this one has a ton more. We should try and store these instead of having to reupdate every loop. And should definitely try and avoid some of the spurious allocations if possible.

Copy link
Member Author

@SamCarlberg SamCarlberg May 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b -> b.getTab().getName() is non-capturing, so only one object is allocated.

The lambda in the forEach is capturing, but could easily be rewritten to be noncapturing. And doing this stream in a conventional loop would be a bit nasty. It is now noncapturing

() -> assertThrows(NullPointerException.class, () -> add("foo", (NetworkTableType) null)),
() -> assertThrows(IllegalArgumentException.class, () -> add("", kDouble)),
() -> assertThrows(IllegalArgumentException.class, () -> add("a/b", kDouble)));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One test per test case please. If you want them grouped use nested test cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, that's new. Would you recommend grouping entries vs sendables, or have something more finely grained?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a parameterized test called invalidInputsToAddEntryTest. Parameters would be the expected exception, the key for the add method, and the object for the add method.

@AustinShalit
Copy link
Member

I'd like to move away from the singleton model for classes like this (and SmartDashboard) because it makes code hard to test. Instead of having static methods in Shuffleboard.java, can we make this an object instead with 2 constructors? One constructor with no arguments would use the default nt table, the second one would take a nt table.

@SamCarlberg
Copy link
Member Author

It's a tradeoff for API simplicity, and to follow the style of SmartDashboard and LiveWindow to make it easier to transition to.

@AustinShalit
Copy link
Member

I don't know if I agree with the API simplicity argument because the rest of our API uses objects rather than statics.

@SamCarlberg
Copy link
Member Author

SmartDashboard and LiveWindow both use statics, so that's what I'm going with here

@frcjenkins
Copy link

frcjenkins commented Jun 26, 2018 via email

@SamCarlberg
Copy link
Member Author

SmartDashboard only references the default NetworkTables instance. Shuffleboard can (and does) use new instances in tests

@SamCarlberg SamCarlberg removed the wip label Jun 26, 2018
private final ShuffleboardContainer m_container;
private final Set<String> m_usedTitles = new HashSet<>();
private final List<ShuffleboardComponent<?>> m_components = new ArrayList<>();
private final Map<String, ShuffleboardLayout> m_layouts = new LinkedHashMap<>(); // NOPMD
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove NOPMD when #1243 is merged

import edu.wpi.first.networktables.NetworkTableInstance;

final class ShuffleboardInstance implements ShuffleboardRoot {
private final Map<String, ShuffleboardTab> m_tabs = new LinkedHashMap<>(); // NOPMD
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove NOPMD when #1243 is merged

* A helper class for Shuffleboard containers to handle common child operations.
*/
final class ContainerHelper {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is wpiformat not catching this blank line @calcmogul?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bug. I reproduced it locally, and it has to do with improper handling of comments (the comment contains the class keyword).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh... lol

Forces buildMetadata to be called on first initialization. Makes layouts work
Add a test to help prevent regressions
So we do not overwrite existing data in NetworkTables when robot code starts
Rename `data` parameter to `defaultValue` on some methods for clarity
* Applies the function {@code func} to all complex widgets in {@code container}. Helper method
* for {@link #applyToAllComplexWidgets}.
*/
private void apply(ShuffleboardContainer container, Consumer<ComplexWidget> func) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be static?

@SamCarlberg
Copy link
Member Author

@PeterJohnson Any further review comments?

@calcmogul
Copy link
Member

Here's a potential fix for clang:

diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp
index ed815dcd1..41dcf301b 100644
--- a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp
+++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp
@@ -33,7 +33,7 @@ ShuffleboardInstance::~ShuffleboardInstance() {}
 
 frc::ShuffleboardTab& ShuffleboardInstance::GetTab(wpi::StringRef title) {
   if (m_impl->tabs.find(title) == m_impl->tabs.end()) {
-    m_impl->tabs.try_emplace(title, std::move(ShuffleboardTab(*this, title)));
+    m_impl->tabs.try_emplace(title, ShuffleboardTab(*this, title));
     m_impl->tabsChanged = true;
   }
   return m_impl->tabs.find(title)->second;
diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h
index 41c195a97..3247d1e79 100644
--- a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h
+++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h
@@ -43,15 +43,15 @@ class ShuffleboardComponentBase : public virtual ShuffleboardValue {
  protected:
   wpi::StringMap<std::shared_ptr<nt::Value>> m_properties;
   bool m_metadataDirty = false;
-
- private:
-  ShuffleboardContainer& m_parent;
-  std::string m_type;
   int m_column = -1;
   int m_row = -1;
   int m_width = -1;
   int m_height = -1;
 
+ private:
+  ShuffleboardContainer& m_parent;
+  std::string m_type;
+
   /**
    * Gets the custom properties for this component. May be null.
    */

@PeterJohnson PeterJohnson merged commit 175c6c1 into wpilibsuite:master Sep 28, 2018
@SamCarlberg SamCarlberg deleted the shuffleboard-integration branch September 28, 2018 15:42
pjbuterbaugh pushed a commit to pjbuterbaugh/allwpilib that referenced this pull request Jun 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants