diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java index 785eb579ad..63ed984b4f 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java +++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java @@ -246,6 +246,26 @@ public final class NetworkTableEntry { return NetworkTablesJNI.getStringArray(m_handle, defaultValue); } + /** + * Checks if a data value is of a type that can be placed in a NetworkTable entry. + * + * @param data the data to check + * @return true if the data can be placed in an entry, false if it cannot + */ + public static boolean isValidDataType(Object data) { + return data instanceof Number + || data instanceof Boolean + || data instanceof String + || data instanceof double[] + || data instanceof Double[] + || data instanceof Number[] + || data instanceof boolean[] + || data instanceof Boolean[] + || data instanceof String[] + || data instanceof byte[] + || data instanceof Byte[]; + } + /** * Sets the entry's value if it does not exist. * diff --git a/ntcore/src/main/native/cpp/Value.cpp b/ntcore/src/main/native/cpp/Value.cpp index f4ae61e86b..61390f033c 100644 --- a/ntcore/src/main/native/cpp/Value.cpp +++ b/ntcore/src/main/native/cpp/Value.cpp @@ -43,6 +43,15 @@ Value::~Value() { delete[] m_val.data.arr_string.arr; } +std::shared_ptr Value::MakeBooleanArray(wpi::ArrayRef value, + uint64_t time) { + auto val = std::make_shared(NT_BOOLEAN_ARRAY, time, private_init()); + val->m_val.data.arr_boolean.arr = new int[value.size()]; + val->m_val.data.arr_boolean.size = value.size(); + std::copy(value.begin(), value.end(), val->m_val.data.arr_boolean.arr); + return val; +} + std::shared_ptr Value::MakeBooleanArray(wpi::ArrayRef value, uint64_t time) { auto val = std::make_shared(NT_BOOLEAN_ARRAY, time, private_init()); diff --git a/ntcore/src/main/native/include/networktables/NetworkTableValue.h b/ntcore/src/main/native/include/networktables/NetworkTableValue.h index ae178c81e6..3aa0c013f5 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableValue.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableValue.h @@ -373,6 +373,17 @@ class Value final { return val; } + /** + * Creates a boolean array entry value. + * + * @param value the value + * @param time if nonzero, the creation time to use (instead of the current + * time) + * @return The entry value + */ + static std::shared_ptr MakeBooleanArray(ArrayRef value, + uint64_t time = 0); + /** * Creates a boolean array entry value. * diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ComplexWidget.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ComplexWidget.cpp new file mode 100644 index 0000000000..19e17bb74d --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/ComplexWidget.cpp @@ -0,0 +1,42 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ComplexWidget.h" + +#include "frc/smartdashboard/Sendable.h" + +using namespace frc; + +ComplexWidget::ComplexWidget(ShuffleboardContainer& parent, + const wpi::Twine& title, Sendable& sendable) + : ShuffleboardValue(title), + ShuffleboardWidget(parent, title), + m_sendable(sendable) {} + +void ComplexWidget::EnableIfActuator() { + if (m_builder.IsActuator()) { + m_builder.StartLiveWindowMode(); + } +} + +void ComplexWidget::DisableIfActuator() { + if (m_builder.IsActuator()) { + m_builder.StopLiveWindowMode(); + } +} + +void ComplexWidget::BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) { + BuildMetadata(metaTable); + if (!m_builderInit) { + m_builder.SetTable(parentTable->GetSubTable(GetTitle())); + m_sendable.InitSendable(m_builder); + m_builder.StartListeners(); + m_builderInit = true; + } + m_builder.UpdateTable(); +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp b/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp new file mode 100644 index 0000000000..ec0111619a --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp @@ -0,0 +1,36 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/Shuffleboard.h" + +#include + +#include "frc/shuffleboard/ShuffleboardTab.h" + +using namespace frc; + +void Shuffleboard::Update() { GetInstance().Update(); } + +ShuffleboardTab& Shuffleboard::GetTab(wpi::StringRef title) { + return GetInstance().GetTab(title); +} + +void Shuffleboard::EnableActuatorWidgets() { + GetInstance().EnableActuatorWidgets(); +} + +void Shuffleboard::DisableActuatorWidgets() { + // Need to update to make sure the sendable builders are initialized + Update(); + GetInstance().DisableActuatorWidgets(); +} + +detail::ShuffleboardInstance& Shuffleboard::GetInstance() { + static detail::ShuffleboardInstance inst( + nt::NetworkTableInstance::GetDefault()); + return inst; +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp new file mode 100644 index 0000000000..76173338f3 --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp @@ -0,0 +1,76 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ShuffleboardComponentBase.h" + +#include + +using namespace frc; + +ShuffleboardComponentBase::ShuffleboardComponentBase( + ShuffleboardContainer& parent, const wpi::Twine& title, + const wpi::Twine& type) + : ShuffleboardValue(title), m_parent(parent) { + wpi::SmallVector storage; + m_type = type.toStringRef(storage); +} + +void ShuffleboardComponentBase::SetType(const wpi::Twine& type) { + wpi::SmallVector storage; + m_type = type.toStringRef(storage); + m_metadataDirty = true; +} + +void ShuffleboardComponentBase::BuildMetadata( + std::shared_ptr metaTable) { + if (!m_metadataDirty) { + return; + } + // Component type + if (GetType() == "") { + metaTable->GetEntry("PreferredComponent").Delete(); + } else { + metaTable->GetEntry("PreferredComponent").ForceSetString(GetType()); + } + + // Tile size + if (m_width <= 0 || m_height <= 0) { + metaTable->GetEntry("Size").Delete(); + } else { + metaTable->GetEntry("Size").SetDoubleArray( + {static_cast(m_width), static_cast(m_height)}); + } + + // Tile position + if (m_column < 0 || m_row < 0) { + metaTable->GetEntry("Position").Delete(); + } else { + metaTable->GetEntry("Position") + .SetDoubleArray( + {static_cast(m_column), static_cast(m_row)}); + } + + // Custom properties + if (GetProperties().size() > 0) { + auto propTable = metaTable->GetSubTable("Properties"); + for (auto& entry : GetProperties()) { + propTable->GetEntry(entry.first()).SetValue(entry.second); + } + } + m_metadataDirty = false; +} + +ShuffleboardContainer& ShuffleboardComponentBase::GetParent() { + return m_parent; +} + +const std::string& ShuffleboardComponentBase::GetType() const { return m_type; } + +const wpi::StringMap>& +ShuffleboardComponentBase::GetProperties() const { + return m_properties; +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp new file mode 100644 index 0000000000..e4a6d04df8 --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp @@ -0,0 +1,165 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ShuffleboardContainer.h" + +#include +#include + +#include "frc/shuffleboard/ComplexWidget.h" +#include "frc/shuffleboard/ShuffleboardComponent.h" +#include "frc/shuffleboard/ShuffleboardLayout.h" +#include "frc/shuffleboard/SimpleWidget.h" + +using namespace frc; + +ShuffleboardContainer::ShuffleboardContainer(const wpi::Twine& title) + : ShuffleboardValue(title) {} + +const std::vector>& +ShuffleboardContainer::GetComponents() const { + return m_components; +} + +ShuffleboardLayout& ShuffleboardContainer::GetLayout(const wpi::Twine& type, + const wpi::Twine& title) { + wpi::SmallVector storage; + auto titleRef = title.toStringRef(storage); + if (m_layouts.count(titleRef) == 0) { + auto layout = std::make_unique(*this, type, titleRef); + auto ptr = layout.get(); + m_components.emplace_back(std::move(layout)); + m_layouts.insert(std::make_pair(titleRef, ptr)); + } + return *m_layouts[titleRef]; +} + +ComplexWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + Sendable& sendable) { + CheckTitle(title); + auto widget = std::make_unique(*this, title, sendable); + auto ptr = widget.get(); + m_components.emplace_back(std::move(widget)); + return *ptr; +} + +ComplexWidget& ShuffleboardContainer::Add(Sendable& sendable) { + if (sendable.GetName().empty()) { + wpi::outs() << "Sendable must have a name\n"; + } + return Add(sendable.GetName(), sendable); +} + +SimpleWidget& ShuffleboardContainer::Add( + const wpi::Twine& title, std::shared_ptr defaultValue) { + CheckTitle(title); + + auto widget = std::make_unique(*this, title); + auto ptr = widget.get(); + widget->GetEntry().SetDefaultValue(defaultValue); + m_components.emplace_back(std::move(widget)); + return *ptr; +} + +SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + bool defaultValue) { + return Add(title, nt::Value::MakeBoolean(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + double defaultValue) { + return Add(title, nt::Value::MakeDouble(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + int defaultValue) { + return Add(title, nt::Value::MakeDouble(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + const wpi::Twine& defaultValue) { + return Add(title, nt::Value::MakeString(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + wpi::ArrayRef defaultValue) { + return Add(title, nt::Value::MakeBooleanArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title, + wpi::ArrayRef defaultValue) { + return Add(title, nt::Value::MakeDoubleArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add( + const wpi::Twine& title, wpi::ArrayRef defaultValue) { + return Add(title, nt::Value::MakeStringArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent( + const wpi::Twine& title, std::shared_ptr defaultValue) { + auto& widget = Add(title, defaultValue); + widget.GetEntry().SetPersistent(); + return widget; +} + +SimpleWidget& ShuffleboardContainer::AddPersistent(const wpi::Twine& title, + bool defaultValue) { + return AddPersistent(title, nt::Value::MakeBoolean(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent(const wpi::Twine& title, + double defaultValue) { + return AddPersistent(title, nt::Value::MakeDouble(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent(const wpi::Twine& title, + int defaultValue) { + return AddPersistent(title, nt::Value::MakeDouble(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent( + const wpi::Twine& title, const wpi::Twine& defaultValue) { + return AddPersistent(title, nt::Value::MakeString(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent( + const wpi::Twine& title, wpi::ArrayRef defaultValue) { + return AddPersistent(title, nt::Value::MakeBooleanArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent( + const wpi::Twine& title, wpi::ArrayRef defaultValue) { + return AddPersistent(title, nt::Value::MakeDoubleArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent( + const wpi::Twine& title, wpi::ArrayRef defaultValue) { + return AddPersistent(title, nt::Value::MakeStringArray(defaultValue)); +} + +void ShuffleboardContainer::EnableIfActuator() { + for (auto& component : GetComponents()) { + component->EnableIfActuator(); + } +} + +void ShuffleboardContainer::DisableIfActuator() { + for (auto& component : GetComponents()) { + component->DisableIfActuator(); + } +} + +void ShuffleboardContainer::CheckTitle(const wpi::Twine& title) { + wpi::SmallVector storage; + auto titleRef = title.toStringRef(storage); + if (m_usedTitles.count(titleRef) > 0) { + wpi::errs() << "Title is already in use: " << title << "\n"; + return; + } + m_usedTitles.insert(titleRef); +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp new file mode 100644 index 0000000000..41dcf301be --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp @@ -0,0 +1,74 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ShuffleboardInstance.h" + +#include +#include +#include + +#include "frc/shuffleboard/Shuffleboard.h" + +using namespace frc::detail; + +struct ShuffleboardInstance::Impl { + wpi::StringMap tabs; + + bool tabsChanged = false; + std::shared_ptr rootTable; + std::shared_ptr rootMetaTable; +}; + +ShuffleboardInstance::ShuffleboardInstance(nt::NetworkTableInstance ntInstance) + : m_impl(new Impl) { + m_impl->rootTable = ntInstance.GetTable(Shuffleboard::kBaseTableName); + m_impl->rootMetaTable = m_impl->rootTable->GetSubTable(".metadata"); +} + +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, ShuffleboardTab(*this, title)); + m_impl->tabsChanged = true; + } + return m_impl->tabs.find(title)->second; +} + +void ShuffleboardInstance::Update() { + if (m_impl->tabsChanged) { + std::vector tabTitles; + for (auto& entry : m_impl->tabs) { + tabTitles.emplace_back(entry.second.GetTitle()); + } + m_impl->rootMetaTable->GetEntry("Tabs").ForceSetStringArray(tabTitles); + m_impl->tabsChanged = false; + } + for (auto& entry : m_impl->tabs) { + auto& tab = entry.second; + tab.BuildInto(m_impl->rootTable, + m_impl->rootMetaTable->GetSubTable(tab.GetTitle())); + } +} + +void ShuffleboardInstance::EnableActuatorWidgets() { + for (auto& entry : m_impl->tabs) { + auto& tab = entry.second; + for (auto& component : tab.GetComponents()) { + component->EnableIfActuator(); + } + } +} + +void ShuffleboardInstance::DisableActuatorWidgets() { + for (auto& entry : m_impl->tabs) { + auto& tab = entry.second; + for (auto& component : tab.GetComponents()) { + component->DisableIfActuator(); + } + } +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardLayout.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardLayout.cpp new file mode 100644 index 0000000000..78281193b7 --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardLayout.cpp @@ -0,0 +1,30 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ShuffleboardLayout.h" + +using namespace frc; + +ShuffleboardLayout::ShuffleboardLayout(ShuffleboardContainer& parent, + const wpi::Twine& name, + const wpi::Twine& type) + : ShuffleboardValue(name), + ShuffleboardComponent(parent, type, name), + ShuffleboardContainer(name) { + m_isLayout = true; +} + +void ShuffleboardLayout::BuildInto( + std::shared_ptr parentTable, + std::shared_ptr metaTable) { + BuildMetadata(metaTable); + auto table = parentTable->GetSubTable(GetTitle()); + table->GetEntry(".type").SetString("ShuffleboardLayout"); + for (auto& component : GetComponents()) { + component->BuildInto(table, metaTable->GetSubTable(component->GetTitle())); + } +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardTab.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardTab.cpp new file mode 100644 index 0000000000..7a8338ea27 --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardTab.cpp @@ -0,0 +1,25 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ShuffleboardTab.h" + +using namespace frc; + +ShuffleboardTab::ShuffleboardTab(ShuffleboardRoot& root, wpi::StringRef title) + : ShuffleboardValue(title), ShuffleboardContainer(title), m_root(root) {} + +ShuffleboardRoot& ShuffleboardTab::GetRoot() { return m_root; } + +void ShuffleboardTab::BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) { + auto tabTable = parentTable->GetSubTable(GetTitle()); + tabTable->GetEntry(".type").SetString("ShuffleboardTab"); + for (auto& component : GetComponents()) { + component->BuildInto(tabTable, + metaTable->GetSubTable(component->GetTitle())); + } +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp b/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp new file mode 100644 index 0000000000..cdcca6302f --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp @@ -0,0 +1,46 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/SimpleWidget.h" + +#include "frc/shuffleboard/Shuffleboard.h" +#include "frc/shuffleboard/ShuffleboardLayout.h" +#include "frc/shuffleboard/ShuffleboardTab.h" + +using namespace frc; + +SimpleWidget::SimpleWidget(ShuffleboardContainer& parent, + const wpi::Twine& title) + : ShuffleboardValue(title), ShuffleboardWidget(parent, title) {} + +nt::NetworkTableEntry SimpleWidget::GetEntry() { + if (!m_entryInitialized) { + ForceGenerate(); + m_entryInitialized = true; + } + return m_entry; +} + +void SimpleWidget::BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) { + BuildMetadata(metaTable); + if (!m_entryInitialized) { + m_entry = parentTable->GetEntry(GetTitle()); + m_entryInitialized = true; + } +} + +void SimpleWidget::ForceGenerate() { + ShuffleboardContainer* parent = &GetParent(); + + while (parent->m_isLayout) { + parent = &(static_cast(parent)->GetParent()); + } + + auto& tab = *static_cast(parent); + tab.GetRoot().Update(); +} diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ComplexWidget.h b/wpilibc/src/main/native/include/frc/shuffleboard/ComplexWidget.h new file mode 100644 index 0000000000..218ed2fd2e --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ComplexWidget.h @@ -0,0 +1,46 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +#include "frc/shuffleboard/ShuffleboardWidget.h" +#include "frc/smartdashboard/SendableBuilder.h" +#include "frc/smartdashboard/SendableBuilderImpl.h" + +namespace frc { + +class Sendable; +class ShuffleboardContainer; + +/** + * A Shuffleboard widget that handles a {@link Sendable} object such as a speed + * controller or sensor. + */ +class ComplexWidget final : public ShuffleboardWidget { + public: + ComplexWidget(ShuffleboardContainer& parent, const wpi::Twine& title, + Sendable& sendable); + + void EnableIfActuator() override; + + void DisableIfActuator() override; + + void BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) override; + + private: + Sendable& m_sendable; + SendableBuilderImpl m_builder; + bool m_builderInit = false; +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h b/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h new file mode 100644 index 0000000000..383a6654de --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h @@ -0,0 +1,111 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "frc/shuffleboard/ShuffleboardInstance.h" + +namespace frc { + +class ShuffleboardTab; + +/** + * The Shuffleboard class provides a mechanism with which data can be added and + * laid out in the Shuffleboard dashboard application from a robot program. Tabs + * and layouts can be specified, as well as choosing which widgets to display + * with and setting properties of these widgets; for example, programmers can + * specify a specific {@code boolean} value to be displayed with a toggle button + * instead of the default colored box, or set custom colors for that box. + * + * For example, displaying a boolean entry with a toggle button: + *
{@code
+ * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ *   .add("My Boolean", false)
+ *   .withWidget("Toggle Button")
+ *   .getEntry();
+ * }
+ * + * Changing the colors of the boolean box: + *
{@code
+ * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ *   .add("My Boolean", false)
+ *   .withWidget("Boolean Box")
+ *   .withProperties(Map.of("colorWhenTrue", "green", "colorWhenFalse",
+ * "maroon")) .getEntry();
+ * }
+ * + * Specifying a parent layout. Note that the layout type must always be + * specified, even if the layout has already been generated by a previously + * defined entry. + *
{@code
+ * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ *   .getLayout("List", "Example List")
+ *   .add("My Boolean", false)
+ *   .withWidget("Toggle Button")
+ *   .getEntry();
+ * }
+ *

+ * + * Teams are encouraged to set up shuffleboard layouts at the start of the robot + * program. + */ +class Shuffleboard final { + public: + /** + * The name of the base NetworkTable into which all Shuffleboard data will be + * added. + */ + static constexpr const char* kBaseTableName = "/Shuffleboard"; + + /** + * Updates all the values in Shuffleboard. Iterative and timed robots are + * pre-configured to call this method in the main robot loop; teams using + * custom robot base classes, or subclass SampleRobot, should make sure to + * call this repeatedly to keep data on the dashboard up to date. + */ + static void Update(); + + /** + * Gets the Shuffleboard tab with the given title, creating it if it does not + * already exist. + * + * @param title the title of the tab + * @return the tab with the given title + */ + static ShuffleboardTab& GetTab(wpi::StringRef title); + + /** + * Enables user control of widgets containing actuators: speed controllers, + * relays, etc. This should only be used when the robot is in test mode. + * IterativeRobotBase and SampleRobot are both configured to call this method + * when entering test mode; most users should not need to use this method + * directly. + */ + static void EnableActuatorWidgets(); + + /** + * Disables user control of widgets containing actuators. For safety reasons, + * actuators should only be controlled while in test mode. IterativeRobotBase + * and SampleRobot are both configured to call this method when exiting in + * test mode; most users should not need to use this method directly. + */ + static void DisableActuatorWidgets(); + + private: + static detail::ShuffleboardInstance& GetInstance(); + + // TODO usage reporting + + Shuffleboard() = default; +}; + +} // namespace frc + +// Make use of references returned by member functions usable +#include "frc/shuffleboard/ShuffleboardTab.h" diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponent.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponent.h new file mode 100644 index 0000000000..d6b4bc4262 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponent.h @@ -0,0 +1,76 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "frc/shuffleboard/ShuffleboardComponentBase.h" + +namespace frc { + +class ShuffleboardContainer; + +/** + * A generic component in Shuffleboard. + * + * @tparam Derived the self type + */ +template +class ShuffleboardComponent : public ShuffleboardComponentBase { + public: + ShuffleboardComponent(ShuffleboardContainer& parent, const wpi::Twine& title, + const wpi::Twine& type = ""); + + virtual ~ShuffleboardComponent() = default; + + /** + * Sets custom properties for this component. Property names are + * case-sensitive and whitespace-insensitive (capitalization and spaces do not + * matter). + * + * @param properties the properties for this component + * @return this component + */ + Derived& WithProperties( + const wpi::StringMap>& properties); + + /** + * Sets the position of this component in the tab. This has no effect if this + * component is inside a layout. + * + * If the position of a single component is set, it is recommended to set the + * positions of all components inside a tab to prevent Shuffleboard + * from automatically placing another component there before the one with the + * specific position is sent. + * + * @param columnIndex the column in the tab to place this component + * @param rowIndex the row in the tab to place this component + * @return this component + */ + Derived& WithPosition(int columnIndex, int rowIndex); + + /** + * Sets the size of this component in the tab. This has no effect if this + * component is inside a layout. + * + * @param width how many columns wide the component should be + * @param height how many rows high the component should be + * @return this component + */ + Derived& WithSize(int width, int height); +}; + +} // namespace frc + +#include "frc/shuffleboard/ShuffleboardComponent.inc" diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponent.inc b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponent.inc new file mode 100644 index 0000000000..f75fb0de93 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponent.inc @@ -0,0 +1,47 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +namespace frc { + +template +ShuffleboardComponent::ShuffleboardComponent( + ShuffleboardContainer& parent, const wpi::Twine& title, + const wpi::Twine& type) + : ShuffleboardValue(title), + ShuffleboardComponentBase(parent, title, type) {} + +template +Derived& ShuffleboardComponent::WithProperties( + const wpi::StringMap>& properties) { + m_properties = properties; + m_metadataDirty = true; + return *static_cast(this); +} + +template +Derived& ShuffleboardComponent::WithPosition(int columnIndex, + int rowIndex) { + m_column = columnIndex; + m_row = rowIndex; + m_metadataDirty = true; + return *static_cast(this); +} + +template +Derived& ShuffleboardComponent::WithSize(int width, int height) { + m_width = width; + m_height = height; + m_metadataDirty = true; + return *static_cast(this); +} + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h new file mode 100644 index 0000000000..3247d1e797 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h @@ -0,0 +1,61 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "frc/shuffleboard/ShuffleboardValue.h" + +namespace frc { + +class ShuffleboardContainer; + +/** + * A shim class to allow storing ShuffleboardComponents in arrays. + */ +class ShuffleboardComponentBase : public virtual ShuffleboardValue { + public: + ShuffleboardComponentBase(ShuffleboardContainer& parent, + const wpi::Twine& title, + const wpi::Twine& type = ""); + + virtual ~ShuffleboardComponentBase() = default; + + void SetType(const wpi::Twine& type); + + void BuildMetadata(std::shared_ptr metaTable); + + ShuffleboardContainer& GetParent(); + + const std::string& GetType() const; + + protected: + wpi::StringMap> m_properties; + bool m_metadataDirty = false; + 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. + */ + const wpi::StringMap>& GetProperties() const; +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h new file mode 100644 index 0000000000..26feba9eb4 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h @@ -0,0 +1,324 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "frc/shuffleboard/ShuffleboardComponentBase.h" +#include "frc/shuffleboard/ShuffleboardValue.h" + +namespace frc { + +class ComplexWidget; +class Sendable; +class ShuffleboardLayout; +class SimpleWidget; + +/** + * Common interface for objects that can contain shuffleboard components. + */ +class ShuffleboardContainer : public virtual ShuffleboardValue { + public: + explicit ShuffleboardContainer(const wpi::Twine& title); + + ShuffleboardContainer(ShuffleboardContainer&& rhs) = default; + + virtual ~ShuffleboardContainer() = default; + + /** + * Gets the components that are direct children of this container. + */ + const std::vector>& GetComponents() + const; + + /** + * Gets the layout with the given type and title, creating it if it does not + * already exist at the time this method is called. + * + * @param type the type of the layout, eg "List" or "Grid" + * @param title the title of the layout + * @return the layout + */ + ShuffleboardLayout& GetLayout(const wpi::Twine& type, + const wpi::Twine& title); + + /** + * Adds a widget to this container to display the given sendable. + * + * @param title the title of the widget + * @param sendable the sendable to display + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + */ + ComplexWidget& Add(const wpi::Twine& title, Sendable& sendable); + + /** + * Adds a widget to this container to display the given sendable. + * + * @param sendable the sendable to display + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title, or if the sendable's name has not been + * specified + */ + ComplexWidget& Add(Sendable& sendable); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, + std::shared_ptr defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, bool defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, double defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, int defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, const wpi::Twine& defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, wpi::ArrayRef defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, + wpi::ArrayRef defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& Add(const wpi::Twine& title, + wpi::ArrayRef defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, + std::shared_ptr defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, bool defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, double defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, int defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, + const wpi::Twine& defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, + wpi::ArrayRef defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, + wpi::ArrayRef defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike {@link #add(String, Object)}, the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see #add(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget& AddPersistent(const wpi::Twine& title, + wpi::ArrayRef defaultValue); + + void EnableIfActuator() override; + + void DisableIfActuator() override; + + protected: + bool m_isLayout = false; + + private: + wpi::SmallSet m_usedTitles; + std::vector> m_components; + wpi::StringMap m_layouts; + + /** + * Adds title to internal set if it hasn't already. + * + * @return True if title isn't in use; false otherwise. + */ + void CheckTitle(const wpi::Twine& title); + + friend class SimpleWidget; +}; + +} // namespace frc + +// Make use of references returned by member functions usable +#include "frc/shuffleboard/ComplexWidget.h" +#include "frc/shuffleboard/ShuffleboardLayout.h" +#include "frc/shuffleboard/SimpleWidget.h" diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardInstance.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardInstance.h new file mode 100644 index 0000000000..9b0a075ea0 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardInstance.h @@ -0,0 +1,37 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "frc/shuffleboard/ShuffleboardRoot.h" +#include "frc/shuffleboard/ShuffleboardTab.h" + +namespace frc { +namespace detail { + +class ShuffleboardInstance final : public ShuffleboardRoot { + public: + explicit ShuffleboardInstance(nt::NetworkTableInstance ntInstance); + virtual ~ShuffleboardInstance(); + + frc::ShuffleboardTab& GetTab(wpi::StringRef title) override; + + void Update() override; + + void EnableActuatorWidgets() override; + + void DisableActuatorWidgets() override; + + private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace detail +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardLayout.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardLayout.h new file mode 100644 index 0000000000..0b5d459371 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardLayout.h @@ -0,0 +1,35 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +#include "frc/shuffleboard/ShuffleboardComponent.h" +#include "frc/shuffleboard/ShuffleboardContainer.h" +#include "frc/smartdashboard/Sendable.h" + +namespace frc { + +/** + * A layout in a Shuffleboard tab. Layouts can contain widgets and other + * layouts. + */ +class ShuffleboardLayout : public ShuffleboardComponent, + public ShuffleboardContainer { + public: + ShuffleboardLayout(ShuffleboardContainer& parent, const wpi::Twine& name, + const wpi::Twine& type); + + void BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) override; +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardRoot.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardRoot.h new file mode 100644 index 0000000000..a8e1c497eb --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardRoot.h @@ -0,0 +1,50 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +namespace frc { + +class ShuffleboardTab; + +/** + * The root of the data placed in Shuffleboard. It contains the tabs, but no + * data is placed directly in the root. + * + * This class is package-private to minimize API surface area. + */ +class ShuffleboardRoot { + public: + /** + * Gets the tab with the given title, creating it if it does not already + * exist. + * + * @param title the title of the tab + * @return the tab with the given title + */ + virtual ShuffleboardTab& GetTab(wpi::StringRef title) = 0; + + /** + * Updates all tabs. + */ + virtual void Update() = 0; + + /** + * Enables all widgets in Shuffleboard that offer user control over actuators. + */ + virtual void EnableActuatorWidgets() = 0; + + /** + * Disables all widgets in Shuffleboard that offer user control over + * actuators. + */ + virtual void DisableActuatorWidgets() = 0; +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardTab.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardTab.h new file mode 100644 index 0000000000..16e6f92288 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardTab.h @@ -0,0 +1,42 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +#include "frc/shuffleboard/ShuffleboardContainer.h" +#include "frc/smartdashboard/Sendable.h" + +namespace frc { + +class ShuffleboardRoot; + +/** + * Represents a tab in the Shuffleboard dashboard. Widgets can be added to the + * tab with {@link #add(Sendable)}, {@link #add(String, Object)}, and + * {@link #add(String, Sendable)}. Widgets can also be added to layouts with + * {@link #getLayout(String, String)}; layouts can be nested arbitrarily deep + * (note that too many levels may make deeper components unusable). + */ +class ShuffleboardTab final : public ShuffleboardContainer { + public: + ShuffleboardTab(ShuffleboardRoot& root, wpi::StringRef title); + + ShuffleboardRoot& GetRoot(); + + void BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) override; + + private: + ShuffleboardRoot& m_root; +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardValue.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardValue.h new file mode 100644 index 0000000000..4a88cc18c5 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardValue.h @@ -0,0 +1,71 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace frc { + +class ShuffleboardValue { + public: + explicit ShuffleboardValue(const wpi::Twine& title) { + wpi::SmallVector storage; + m_title = title.toStringRef(storage); + } + + virtual ~ShuffleboardValue() = default; + + /** + * Gets the title of this Shuffleboard value. + */ + wpi::StringRef GetTitle() const { return m_title; } + + /** + * Builds the entries for this value. + * + * @param parentTable The table containing all the data for the parent. Values + * that require a complex entry or table structure should + * call {@code parentTable.getSubtable(getTitle())} to get + * the table to put data into. Values that only use a + * single entry should call + * {@code parentTable.getEntry(getTitle())} to get that + * entry. + * @param metaTable The table containing all the metadata for this value and + * its sub-values + */ + virtual void BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) = 0; + + /** + * Enables user control of this widget in the Shuffleboard application. + * + * This method is package-private to prevent users from enabling control + * themselves. Has no effect if the sendable is not marked as an actuator with + * {@link SendableBuilder#setActuator}. + */ + virtual void EnableIfActuator() {} + + /** + * Disables user control of this widget in the Shuffleboard application. + * + * This method is package-private to prevent users from enabling control + * themselves. Has no effect if the sendable is not marked as an actuator with + * {@link SendableBuilder#setActuator}. + */ + virtual void DisableIfActuator() {} + + private: + std::string m_title; +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardWidget.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardWidget.h new file mode 100644 index 0000000000..6ec520bf8a --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardWidget.h @@ -0,0 +1,46 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "frc/shuffleboard/ShuffleboardComponent.h" + +namespace frc { + +class ShuffleboardContainer; + +/** + * Abstract superclass for widgets. + * + *

This class is package-private to minimize API surface area. + * + * @tparam Derived the self type + */ +template +class ShuffleboardWidget + : public ShuffleboardComponent> { + public: + ShuffleboardWidget(ShuffleboardContainer& parent, const wpi::Twine& title) + : ShuffleboardValue(title), + ShuffleboardComponent>(parent, title) {} + + /** + * Sets the type of widget used to display the data. If not set, the default + * widget type will be used. + * + * @param widgetType the type of the widget used to display the data + * @return this widget object + */ + Derived& WithWidget(const wpi::Twine& widgetType) { + this->SetType(widgetType); + return *static_cast(this); + } +}; + +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h b/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h new file mode 100644 index 0000000000..2f1cea9de3 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h @@ -0,0 +1,45 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include +#include + +#include "frc/shuffleboard/ShuffleboardWidget.h" + +namespace frc { + +class ShuffleboardContainer; + +/** + * A Shuffleboard widget that handles a single data point such as a number or + * string. + */ +class SimpleWidget final : public ShuffleboardWidget { + public: + SimpleWidget(ShuffleboardContainer& parent, const wpi::Twine& title); + + /** + * Gets the NetworkTable entry that contains the data for this widget. + */ + nt::NetworkTableEntry GetEntry(); + + void BuildInto(std::shared_ptr parentTable, + std::shared_ptr metaTable) override; + + private: + nt::NetworkTableEntry m_entry; + bool m_entryInitialized = false; + + void ForceGenerate(); +}; + +} // namespace frc diff --git a/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/MockActuatorSendable.cpp b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/MockActuatorSendable.cpp new file mode 100644 index 0000000000..3c9e411fd6 --- /dev/null +++ b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/MockActuatorSendable.cpp @@ -0,0 +1,19 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "shuffleboard/MockActuatorSendable.h" + +using namespace frc; + +MockActuatorSendable::MockActuatorSendable(wpi::StringRef name) + : SendableBase(false) { + SetName(name); +} + +void MockActuatorSendable::InitSendable(SendableBuilder& builder) { + builder.SetActuator(true); +} diff --git a/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp new file mode 100644 index 0000000000..6e0303bc17 --- /dev/null +++ b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp @@ -0,0 +1,105 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/ShuffleboardInstance.h" // NOLINT(build/include_order) + +#include +#include + +#include +#include + +#include "frc/shuffleboard/ShuffleboardInstance.h" +#include "gtest/gtest.h" +#include "shuffleboard/MockActuatorSendable.h" + +using namespace frc; + +class ShuffleboardInstanceTest : public testing::Test { + void SetUp() override { + m_ntInstance = nt::NetworkTableInstance::Create(); + m_shuffleboardInstance = + std::make_unique(m_ntInstance); + } + + protected: + nt::NetworkTableInstance m_ntInstance; + std::unique_ptr m_shuffleboardInstance; +}; + +TEST_F(ShuffleboardInstanceTest, PathFluent) { + auto entry = m_shuffleboardInstance->GetTab("Tab Title") + .GetLayout("List", "List Layout") + .Add("Data", "string") + .WithWidget("Text View") + .GetEntry(); + + EXPECT_EQ("string", entry.GetString("")) << "Wrong entry value"; + EXPECT_EQ("/Shuffleboard/Tab Title/List Layout/Data", entry.GetName()) + << "Entry path generated incorrectly"; +} + +TEST_F(ShuffleboardInstanceTest, NestedLayoutsFluent) { + auto entry = m_shuffleboardInstance->GetTab("Tab") + .GetLayout("List", "First") + .GetLayout("List", "Second") + .GetLayout("List", "Third") + .GetLayout("List", "Fourth") + .Add("Value", "string") + .GetEntry(); + + EXPECT_EQ("string", entry.GetString("")) << "Wrong entry value"; + EXPECT_EQ("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", + entry.GetName()) + << "Entry path generated incorrectly"; +} + +TEST_F(ShuffleboardInstanceTest, NestedLayoutsOop) { + ShuffleboardTab& tab = m_shuffleboardInstance->GetTab("Tab"); + ShuffleboardLayout& first = tab.GetLayout("List", "First"); + ShuffleboardLayout& second = first.GetLayout("List", "Second"); + ShuffleboardLayout& third = second.GetLayout("List", "Third"); + ShuffleboardLayout& fourth = third.GetLayout("List", "Fourth"); + SimpleWidget& widget = fourth.Add("Value", "string"); + auto entry = widget.GetEntry(); + + EXPECT_EQ("string", entry.GetString("")) << "Wrong entry value"; + EXPECT_EQ("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", + entry.GetName()) + << "Entry path generated incorrectly"; +} + +TEST_F(ShuffleboardInstanceTest, LayoutTypeIsSet) { + std::string layoutType = "Type"; + m_shuffleboardInstance->GetTab("Tab").GetLayout(layoutType, "Title"); + m_shuffleboardInstance->Update(); + nt::NetworkTableEntry entry = m_ntInstance.GetEntry( + "/Shuffleboard/.metadata/Tab/Title/PreferredComponent"); + EXPECT_EQ(layoutType, entry.GetString("Not Set")) << "Layout type not set"; +} + +TEST_F(ShuffleboardInstanceTest, NestedActuatoWidgetsAreDisabled) { + MockActuatorSendable sendable("Actuator"); + m_shuffleboardInstance->GetTab("Tab") + .GetLayout("Layout", "Title") + .Add(sendable); + auto controllableEntry = + m_ntInstance.GetEntry("/Shuffleboard/Tab/Title/Actuator/.controllable"); + m_shuffleboardInstance->Update(); + + // Note: we use the unsafe `GetBoolean()` method because if the value is NOT + // a boolean, or if it is not present, then something has clearly gone very, + // very wrong + bool controllable = controllableEntry.GetValue()->GetBoolean(); + // Sanity check + EXPECT_TRUE(controllable) + << "The nested actuator widget should be enabled by default"; + m_shuffleboardInstance->DisableActuatorWidgets(); + controllable = controllableEntry.GetValue()->GetBoolean(); + EXPECT_FALSE(controllable) + << "The nested actuator widget should have been disabled"; +} diff --git a/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardTabTest.cpp b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardTabTest.cpp new file mode 100644 index 0000000000..d1567577c5 --- /dev/null +++ b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardTabTest.cpp @@ -0,0 +1,115 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include +#include +#include + +#include +#include + +#include "frc/commands/InstantCommand.h" +#include "frc/shuffleboard/ShuffleboardInstance.h" +#include "frc/shuffleboard/ShuffleboardTab.h" +#include "frc/smartdashboard/Sendable.h" +#include "gtest/gtest.h" + +using namespace frc; + +class ShuffleboardTabTest : public testing::Test { + void SetUp() override { + m_ntInstance = nt::NetworkTableInstance::Create(); + m_instance = std::make_unique(m_ntInstance); + m_tab = &(m_instance->GetTab("Tab")); + } + + protected: + nt::NetworkTableInstance m_ntInstance; + ShuffleboardTab* m_tab; + std::unique_ptr m_instance; +}; + +TEST_F(ShuffleboardTabTest, AddDouble) { + auto entry = m_tab->Add("Double", 1.0).GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/Double", entry.GetName()); + EXPECT_FLOAT_EQ(1.0, entry.GetValue()->GetDouble()); +} + +TEST_F(ShuffleboardTabTest, AddInteger) { + auto entry = m_tab->Add("Int", 1).GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/Int", entry.GetName()); + EXPECT_FLOAT_EQ(1.0, entry.GetValue()->GetDouble()); +} + +TEST_F(ShuffleboardTabTest, AddBoolean) { + auto entry = m_tab->Add("Bool", false).GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/Bool", entry.GetName()); + EXPECT_FALSE(entry.GetValue()->GetBoolean()); +} + +TEST_F(ShuffleboardTabTest, AddString) { + auto entry = m_tab->Add("String", "foobar").GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/String", entry.GetName()); + EXPECT_EQ("foobar", entry.GetValue()->GetString()); +} + +TEST_F(ShuffleboardTabTest, AddNamedSendableWithProperties) { + InstantCommand sendable("Command"); + std::string widgetType = "Command Widget"; + wpi::StringMap> map; + map.try_emplace("foo", nt::Value::MakeDouble(1234)); + map.try_emplace("bar", nt::Value::MakeString("baz")); + m_tab->Add(sendable).WithWidget(widgetType).WithProperties(map); + + m_instance->Update(); + std::string meta = "/Shuffleboard/.metadata/Tab/Command"; + + EXPECT_EQ(1234, m_ntInstance.GetEntry(meta + "/Properties/foo").GetDouble(-1)) + << "Property 'foo' not set correctly"; + EXPECT_EQ("baz", + m_ntInstance.GetEntry(meta + "/Properties/bar").GetString("")) + << "Property 'bar' not set correctly"; + EXPECT_EQ(widgetType, + m_ntInstance.GetEntry(meta + "/PreferredComponent").GetString("")) + << "Preferred component not set correctly"; +} + +TEST_F(ShuffleboardTabTest, AddNumberArray) { + std::array expect = {1.0, 2.0, 3.0}; + auto entry = m_tab->Add("DoubleArray", expect).GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/DoubleArray", entry.GetName()); + + auto actual = entry.GetValue()->GetDoubleArray(); + EXPECT_EQ(sizeof(expect), sizeof(actual)); + for (size_t i = 0; i < sizeof(expect); i++) { + EXPECT_FLOAT_EQ(expect[i], actual[i]); + } +} + +TEST_F(ShuffleboardTabTest, AddBooleanArray) { + std::array expect = {true, false}; + auto entry = m_tab->Add("BoolArray", expect).GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/BoolArray", entry.GetName()); + + auto actual = entry.GetValue()->GetBooleanArray(); + EXPECT_EQ(sizeof(expect), sizeof(actual)); + for (size_t i = 0; i < sizeof(expect); i++) { + EXPECT_EQ(expect[i], actual[i]); + } +} + +TEST_F(ShuffleboardTabTest, AddStringArray) { + std::array expect = {"foo", "bar"}; + auto entry = m_tab->Add("StringArray", expect).GetEntry(); + EXPECT_EQ("/Shuffleboard/Tab/StringArray", entry.GetName()); + + auto actual = entry.GetValue()->GetStringArray(); + EXPECT_EQ(sizeof(expect), sizeof(actual)); + for (size_t i = 0; i < sizeof(expect); i++) { + EXPECT_EQ(expect[i], actual[i]); + } +} diff --git a/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardTest.cpp b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardTest.cpp new file mode 100644 index 0000000000..d39d59d399 --- /dev/null +++ b/wpilibcIntegrationTests/src/main/native/cpp/shuffleboard/ShuffleboardTest.cpp @@ -0,0 +1,20 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/Shuffleboard.h" +#include "frc/shuffleboard/ShuffleboardTab.h" +#include "gtest/gtest.h" + +using namespace frc; + +class ShuffleboardTest : public testing::Test {}; + +TEST_F(ShuffleboardTest, TabObjectsCached) { + ShuffleboardTab& tab1 = Shuffleboard::GetTab("testTabObjectsCached"); + ShuffleboardTab& tab2 = Shuffleboard::GetTab("testTabObjectsCached"); + EXPECT_EQ(&tab1, &tab2) << "Tab objects were not cached"; +} diff --git a/wpilibcIntegrationTests/src/main/native/include/shuffleboard/MockActuatorSendable.h b/wpilibcIntegrationTests/src/main/native/include/shuffleboard/MockActuatorSendable.h new file mode 100644 index 0000000000..f56215c42e --- /dev/null +++ b/wpilibcIntegrationTests/src/main/native/include/shuffleboard/MockActuatorSendable.h @@ -0,0 +1,27 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "frc/smartdashboard/SendableBase.h" +#include "frc/smartdashboard/SendableBuilder.h" + +namespace frc { + +/** + * A mock sendable that marks itself as an actuator. + */ +class MockActuatorSendable : public SendableBase { + public: + explicit MockActuatorSendable(wpi::StringRef name); + + void InitSendable(SendableBuilder& builder) override; +}; + +} // namespace frc diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java index 2df5ac782c..e2a7f489f0 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java @@ -9,6 +9,7 @@ package edu.wpi.first.wpilibj; import edu.wpi.first.hal.HAL; import edu.wpi.first.wpilibj.livewindow.LiveWindow; +import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; /** @@ -200,6 +201,7 @@ public abstract class IterativeRobotBase extends RobotBase { // or from power-on. if (m_lastMode != Mode.kDisabled) { LiveWindow.setEnabled(false); + Shuffleboard.disableActuatorWidgets(); disabledInit(); m_watchdog.addEpoch("disabledInit()"); m_lastMode = Mode.kDisabled; @@ -213,6 +215,7 @@ public abstract class IterativeRobotBase extends RobotBase { // mode or from power-on. if (m_lastMode != Mode.kAutonomous) { LiveWindow.setEnabled(false); + Shuffleboard.disableActuatorWidgets(); autonomousInit(); m_watchdog.addEpoch("autonomousInit()"); m_lastMode = Mode.kAutonomous; @@ -226,6 +229,7 @@ public abstract class IterativeRobotBase extends RobotBase { // from power-on. if (m_lastMode != Mode.kTeleop) { LiveWindow.setEnabled(false); + Shuffleboard.disableActuatorWidgets(); teleopInit(); m_watchdog.addEpoch("teleopInit()"); m_lastMode = Mode.kTeleop; @@ -239,6 +243,7 @@ public abstract class IterativeRobotBase extends RobotBase { // power-on. if (m_lastMode != Mode.kTest) { LiveWindow.setEnabled(true); + Shuffleboard.enableActuatorWidgets(); testInit(); m_watchdog.addEpoch("testInit()"); m_lastMode = Mode.kTest; @@ -255,6 +260,7 @@ public abstract class IterativeRobotBase extends RobotBase { SmartDashboard.updateValues(); LiveWindow.updateValues(); + Shuffleboard.update(); // Warn on loop time overruns if (m_watchdog.isExpired()) { diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java index ed978492e0..a82467076a 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java @@ -23,6 +23,7 @@ import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.HALUtil; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.wpilibj.livewindow.LiveWindow; +import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; import edu.wpi.first.wpilibj.util.WPILibVersion; /** @@ -91,6 +92,7 @@ public abstract class RobotBase implements AutoCloseable { inst.getTable("LiveWindow").getSubTable(".status").getEntry("LW Enabled").setBoolean(false); LiveWindow.setEnabled(false); + Shuffleboard.disableActuatorWidgets(); } @Deprecated diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SampleRobot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SampleRobot.java index feec9c2124..6c13013cbb 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SampleRobot.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SampleRobot.java @@ -11,6 +11,7 @@ import edu.wpi.first.hal.FRCNetComm.tInstances; import edu.wpi.first.hal.FRCNetComm.tResourceType; import edu.wpi.first.hal.HAL; import edu.wpi.first.wpilibj.livewindow.LiveWindow; +import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; /** * A simple robot base class that knows the standard FRC competition states (disabled, autonomous, @@ -142,6 +143,7 @@ public class SampleRobot extends RobotBase { } } else if (isTest()) { LiveWindow.setEnabled(true); + Shuffleboard.enableActuatorWidgets(); m_ds.InTest(true); test(); m_ds.InTest(false); @@ -149,6 +151,7 @@ public class SampleRobot extends RobotBase { Timer.delay(0.01); } LiveWindow.setEnabled(false); + Shuffleboard.disableActuatorWidgets(); } else { m_ds.InOperatorControl(true); operatorControl(); diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java new file mode 100644 index 0000000000..d56fde84ba --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.wpilibj.Sendable; +import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder; +import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl; + +/** + * A Shuffleboard widget that handles a {@link Sendable} object such as a speed controller or + * sensor. + */ +public final class ComplexWidget extends ShuffleboardWidget { + private final Sendable m_sendable; + private SendableBuilderImpl m_builder; + + ComplexWidget(ShuffleboardContainer parent, String title, Sendable sendable) { + super(parent, title); + m_sendable = sendable; + } + + @Override + public void buildInto(NetworkTable parentTable, NetworkTable metaTable) { + buildMetadata(metaTable); + if (m_builder == null) { + m_builder = new SendableBuilderImpl(); + m_builder.setTable(parentTable.getSubTable(getTitle())); + m_sendable.initSendable(m_builder); + m_builder.startListeners(); + } + m_builder.updateTable(); + } + + /** + * Enables user control of this widget in the Shuffleboard application. This method is + * package-private to prevent users from enabling control themselves. Has no effect if the + * sendable is not marked as an actuator with {@link SendableBuilder#setActuator}. + */ + void enableIfActuator() { + if (m_builder.isActuator()) { + m_builder.startLiveWindowMode(); + } + } + + /** + * Disables user control of this widget in the Shuffleboard application. This method is + * package-private to prevent users from enabling control themselves. Has no effect if the + * sendable is not marked as an actuator with {@link SendableBuilder#setActuator}. + */ + void disableIfActuator() { + if (m_builder.isActuator()) { + m_builder.stopLiveWindowMode(); + } + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java new file mode 100644 index 0000000000..d88c57b532 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java @@ -0,0 +1,87 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.wpilibj.Sendable; + +/** + * A helper class for Shuffleboard containers to handle common child operations. + */ +final class ContainerHelper { + private final ShuffleboardContainer m_container; + private final Set m_usedTitles = new HashSet<>(); + private final List> m_components = new ArrayList<>(); + private final Map m_layouts = new LinkedHashMap<>(); + + ContainerHelper(ShuffleboardContainer container) { + m_container = container; + } + + List> getComponents() { + return m_components; + } + + ShuffleboardLayout getLayout(String type, String title) { + if (!m_layouts.containsKey(title)) { + ShuffleboardLayout layout = new ShuffleboardLayout(m_container, type, title); + m_components.add(layout); + m_layouts.put(title, layout); + } + return m_layouts.get(title); + } + + ComplexWidget add(String title, Sendable sendable) { + checkTitle(title); + ComplexWidget widget = new ComplexWidget(m_container, title, sendable); + m_components.add(widget); + return widget; + } + + ComplexWidget add(Sendable sendable) throws IllegalArgumentException { + if (sendable.getName() == null || sendable.getName().isEmpty()) { + throw new IllegalArgumentException("Sendable must have a name"); + } + return add(sendable.getName(), sendable); + } + + SimpleWidget add(String title, Object defaultValue) { + Objects.requireNonNull(title, "Title cannot be null"); + Objects.requireNonNull(defaultValue, "Default value cannot be null"); + checkTitle(title); + checkNtType(defaultValue); + + SimpleWidget widget = new SimpleWidget(m_container, title); + m_components.add(widget); + widget.getEntry().setDefaultValue(defaultValue); + return widget; + } + + private static void checkNtType(Object data) { + if (!NetworkTableEntry.isValidDataType(data)) { + throw new IllegalArgumentException( + "Cannot add data of type " + data.getClass().getName() + " to Shuffleboard"); + } + } + + private void checkTitle(String title) { + if (m_usedTitles.contains(title)) { + throw new IllegalArgumentException("Title is already in use: " + title); + } + m_usedTitles.add(title); + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java new file mode 100644 index 0000000000..be742c7003 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java @@ -0,0 +1,104 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import edu.wpi.first.networktables.NetworkTableInstance; + +/** + * The Shuffleboard class provides a mechanism with which data can be added and laid out in the + * Shuffleboard dashboard application from a robot program. Tabs and layouts can be specified, as + * well as choosing which widgets to display with and setting properties of these widgets; for + * example, programmers can specify a specific {@code boolean} value to be displayed with a toggle + * button instead of the default colored box, or set custom colors for that box. + * + *

For example, displaying a boolean entry with a toggle button: + *

{@code
+ * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ *   .add("My Boolean", false)
+ *   .withWidget("Toggle Button")
+ *   .getEntry();
+ * }
+ * + * Changing the colors of the boolean box: + *
{@code
+ * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ *   .add("My Boolean", false)
+ *   .withWidget("Boolean Box")
+ *   .withProperties(Map.of("colorWhenTrue", "green", "colorWhenFalse", "maroon"))
+ *   .getEntry();
+ * }
+ * + * Specifying a parent layout. Note that the layout type must always be specified, even if + * the layout has already been generated by a previously defined entry. + *
{@code
+ * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ *   .getLayout("List", "Example List")
+ *   .add("My Boolean", false)
+ *   .withWidget("Toggle Button")
+ *   .getEntry();
+ * }
+ *

+ * + *

Teams are encouraged to set up shuffleboard layouts at the start of the robot program.

+ */ +public final class Shuffleboard { + /** + * The name of the base NetworkTable into which all Shuffleboard data will be added. + */ + public static final String kBaseTableName = "/Shuffleboard"; + + private static final ShuffleboardRoot root = + new ShuffleboardInstance(NetworkTableInstance.getDefault()); + + // TODO usage reporting + + private Shuffleboard() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Updates all the values in Shuffleboard. Iterative and timed robots are pre-configured to call + * this method in the main robot loop; teams using custom robot base classes, or subclass + * SampleRobot, should make sure to call this repeatedly to keep data on the dashboard up to date. + */ + public static void update() { + root.update(); + } + + /** + * Gets the Shuffleboard tab with the given title, creating it if it does not already exist. + * + * @param title the title of the tab + * @return the tab with the given title + */ + public static ShuffleboardTab getTab(String title) { + return root.getTab(title); + } + + /** + * Enables user control of widgets containing actuators: speed controllers, relays, etc. This + * should only be used when the robot is in test mode. IterativeRobotBase and SampleRobot are + * both configured to call this method when entering test mode; most users should not need to use + * this method directly. + */ + public static void enableActuatorWidgets() { + root.enableActuatorWidgets(); + } + + /** + * Disables user control of widgets containing actuators. For safety reasons, actuators should + * only be controlled while in test mode. IterativeRobotBase and SampleRobot are both configured + * to call this method when exiting in test mode; most users should not need to use + * this method directly. + */ + public static void disableActuatorWidgets() { + update(); // Need to update to make sure the sendable builders are initialized + root.disableActuatorWidgets(); + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java new file mode 100644 index 0000000000..ba87d826c3 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java @@ -0,0 +1,147 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.Map; +import java.util.Objects; + +import edu.wpi.first.networktables.NetworkTable; + +/** + * A generic component in Shuffleboard. + * + * @param the self type + */ +public abstract class ShuffleboardComponent> + implements ShuffleboardValue { + private final ShuffleboardContainer m_parent; + private final String m_title; + private String m_type; + private Map m_properties; + private boolean m_metadataDirty = true; + private int m_column = -1; + private int m_row = -1; + private int m_width = -1; + private int m_height = -1; + + protected ShuffleboardComponent(ShuffleboardContainer parent, String title, String type) { + m_parent = Objects.requireNonNull(parent, "Parent cannot be null"); + m_title = Objects.requireNonNull(title, "Title cannot be null"); + m_type = type; + } + + protected ShuffleboardComponent(ShuffleboardContainer parent, String title) { + this(parent, title, null); + } + + public final ShuffleboardContainer getParent() { + return m_parent; + } + + protected final void setType(String type) { + m_type = type; + m_metadataDirty = true; + } + + public final String getType() { + return m_type; + } + + @Override + public final String getTitle() { + return m_title; + } + + /** + * Gets the custom properties for this component. May be null. + */ + final Map getProperties() { + return m_properties; + } + + /** + * Sets custom properties for this component. Property names are case- and whitespace-insensitive + * (capitalization and spaces do not matter). + * + * @param properties the properties for this component + * @return this component + */ + public final C withProperties(Map properties) { + m_properties = properties; + m_metadataDirty = true; + return (C) this; + } + + /** + * Sets the position of this component in the tab. This has no effect if this component is inside + * a layout. + * + *

If the position of a single component is set, it is recommended to set the positions of + * all components inside a tab to prevent Shuffleboard from automatically placing another + * component there before the one with the specific position is sent. + * + * @param columnIndex the column in the tab to place this component + * @param rowIndex the row in the tab to place this component + * @return this component + */ + public final C withPosition(int columnIndex, int rowIndex) { + m_column = columnIndex; + m_row = rowIndex; + m_metadataDirty = true; + return (C) this; + } + + /** + * Sets the size of this component in the tab. This has no effect if this component is inside a + * layout. + * + * @param width how many columns wide the component should be + * @param height how many rows high the component should be + * @return this component + */ + public final C withSize(int width, int height) { + m_width = width; + m_height = height; + m_metadataDirty = true; + return (C) this; + } + + protected final void buildMetadata(NetworkTable metaTable) { + if (!m_metadataDirty) { + return; + } + // Component type + if (getType() == null) { + metaTable.getEntry("PreferredComponent").delete(); + } else { + metaTable.getEntry("PreferredComponent").forceSetString(getType()); + } + + // Tile size + if (m_width <= 0 || m_height <= 0) { + metaTable.getEntry("Size").delete(); + } else { + metaTable.getEntry("Size").setDoubleArray(new double[]{m_width, m_height}); + } + + // Tile position + if (m_column < 0 || m_row < 0) { + metaTable.getEntry("Position").delete(); + } else { + metaTable.getEntry("Position").setDoubleArray(new double[]{m_column, m_row}); + } + + // Custom properties + if (getProperties() != null) { + NetworkTable propTable = metaTable.getSubTable("Properties"); + getProperties().forEach((name, value) -> propTable.getEntry(name).setValue(value)); + } + m_metadataDirty = false; + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java new file mode 100644 index 0000000000..aa50f9f758 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java @@ -0,0 +1,86 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.List; + +import edu.wpi.first.wpilibj.Sendable; + +/** + * Common interface for objects that can contain shuffleboard components. + */ +public interface ShuffleboardContainer extends ShuffleboardValue { + + /** + * Gets the components that are direct children of this container. + */ + List> getComponents(); + + /** + * Gets the layout with the given type and title, creating it if it does not already exist at the + * time this method is called. + * + * @param type the type of the layout, eg "List" or "Grid" + * @param title the title of the layout + * @return the layout + */ + ShuffleboardLayout getLayout(String type, String title); + + /** + * Adds a widget to this container to display the given sendable. + * + * @param title the title of the widget + * @param sendable the sendable to display + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this container with the given + * title + */ + ComplexWidget add(String title, Sendable sendable) throws IllegalArgumentException; + + /** + * Adds a widget to this container to display the given sendable. + * + * @param sendable the sendable to display + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this container with the given + * title, or if the sendable's name has not been specified + */ + ComplexWidget add(Sendable sendable); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this container with the given + * title + * @see #addPersistent(String, Object) add(String title, Object defaultValue) + */ + SimpleWidget add(String title, Object defaultValue) throws IllegalArgumentException; + + /** + * Adds a widget to this container to display a simple piece of data. Unlike + * {@link #add(String, Object)}, the value in the widget will be saved on the robot and will be + * used when the robot program next starts rather than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this container with the given + * title + * @see #add(String, Object) add(String title, Object defaultValue) + */ + default SimpleWidget addPersistent(String title, Object defaultValue) + throws IllegalArgumentException { + SimpleWidget widget = add(title, defaultValue); + widget.getEntry().setPersistent(); + return widget; + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java new file mode 100644 index 0000000000..81d1c68dcf --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java @@ -0,0 +1,93 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; + +final class ShuffleboardInstance implements ShuffleboardRoot { + private final Map m_tabs = new LinkedHashMap<>(); + + private boolean m_tabsChanged = false; // NOPMD redundant field initializer + private final NetworkTable m_rootTable; + private final NetworkTable m_rootMetaTable; + + ShuffleboardInstance(NetworkTableInstance ntInstance) { + Objects.requireNonNull(ntInstance, "NetworkTable instance cannot be null"); + m_rootTable = ntInstance.getTable(Shuffleboard.kBaseTableName); + m_rootMetaTable = m_rootTable.getSubTable(".metadata"); + } + + @Override + public ShuffleboardTab getTab(String title) { + Objects.requireNonNull(title, "Tab title cannot be null"); + if (!m_tabs.containsKey(title)) { + m_tabs.put(title, new ShuffleboardTab(this, title)); + m_tabsChanged = true; + } + return m_tabs.get(title); + } + + @Override + public void update() { + if (m_tabsChanged) { + String[] tabTitles = m_tabs.values() + .stream() + .map(ShuffleboardTab::getTitle) + .toArray(String[]::new); + m_rootMetaTable.getEntry("Tabs").forceSetStringArray(tabTitles); + m_tabsChanged = false; + } + for (ShuffleboardTab tab : m_tabs.values()) { + String title = tab.getTitle(); + tab.buildInto(m_rootTable, m_rootMetaTable.getSubTable(title)); + } + } + + @Override + public void enableActuatorWidgets() { + applyToAllComplexWidgets(ComplexWidget::enableIfActuator); + } + + @Override + public void disableActuatorWidgets() { + applyToAllComplexWidgets(ComplexWidget::disableIfActuator); + } + + /** + * Applies the function {@code func} to all complex widgets in this root, regardless of how they + * are nested. + * + * @param func the function to apply to all complex widgets + */ + private void applyToAllComplexWidgets(Consumer func) { + for (ShuffleboardTab tab : m_tabs.values()) { + apply(tab, func); + } + } + + /** + * Applies the function {@code func} to all complex widgets in {@code container}. Helper method + * for {@link #applyToAllComplexWidgets}. + */ + private void apply(ShuffleboardContainer container, Consumer func) { + for (ShuffleboardComponent component : container.getComponents()) { + if (component instanceof ComplexWidget) { + func.accept((ComplexWidget) component); + } + if (component instanceof ShuffleboardContainer) { + apply((ShuffleboardContainer) component, func); + } + } + } +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java new file mode 100644 index 0000000000..7c41035ed6 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.List; +import java.util.Objects; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.wpilibj.Sendable; + +/** + * A layout in a Shuffleboard tab. Layouts can contain widgets and other layouts. + */ +public class ShuffleboardLayout extends ShuffleboardComponent + implements ShuffleboardContainer { + private final ContainerHelper m_helper = new ContainerHelper(this); + + ShuffleboardLayout(ShuffleboardContainer parent, String name, String type) { + super(parent, Objects.requireNonNull(type, "Layout type must be specified"), name); + } + + @Override + public List> getComponents() { + return m_helper.getComponents(); + } + + @Override + public ShuffleboardLayout getLayout(String type, String title) { + return m_helper.getLayout(type, title); + } + + @Override + public ComplexWidget add(String title, Sendable sendable) throws IllegalArgumentException { + return m_helper.add(title, sendable); + } + + @Override + public ComplexWidget add(Sendable sendable) throws IllegalArgumentException { + return m_helper.add(sendable); + } + + @Override + public SimpleWidget add(String title, Object defaultValue) throws IllegalArgumentException { + return m_helper.add(title, defaultValue); + } + + @Override + public void buildInto(NetworkTable parentTable, NetworkTable metaTable) { + buildMetadata(metaTable); + NetworkTable table = parentTable.getSubTable(getTitle()); + table.getEntry(".type").setString("ShuffleboardLayout"); + for (ShuffleboardComponent component : getComponents()) { + component.buildInto(table, metaTable.getSubTable(component.getTitle())); + } + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java new file mode 100644 index 0000000000..18da1e8444 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java @@ -0,0 +1,41 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +/** + * The root of the data placed in Shuffleboard. It contains the tabs, but no data is placed + * directly in the root. + * + *

This class is package-private to minimize API surface area. + */ +interface ShuffleboardRoot { + + /** + * Gets the tab with the given title, creating it if it does not already exist. + * + * @param title the title of the tab + * @return the tab with the given title + */ + ShuffleboardTab getTab(String title); + + /** + * Updates all tabs. + */ + void update(); + + /** + * Enables all widgets in Shuffleboard that offer user control over actuators. + */ + void enableActuatorWidgets(); + + /** + * Disables all widgets in Shuffleboard that offer user control over actuators. + */ + void disableActuatorWidgets(); + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java new file mode 100644 index 0000000000..83b897a3e9 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java @@ -0,0 +1,74 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.List; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.wpilibj.Sendable; + +/** + * Represents a tab in the Shuffleboard dashboard. Widgets can be added to the tab with + * {@link #add(Sendable)}, {@link #add(String, Object)}, and {@link #add(String, Sendable)}. Widgets + * can also be added to layouts with {@link #getLayout(String, String)}; layouts can be nested + * arbitrarily deep (note that too many levels may make deeper components unusable). + */ +public final class ShuffleboardTab implements ShuffleboardContainer { + private final ContainerHelper m_helper = new ContainerHelper(this); + private final ShuffleboardRoot m_root; + private final String m_title; + + ShuffleboardTab(ShuffleboardRoot root, String title) { + m_root = root; + m_title = title; + } + + @Override + public String getTitle() { + return m_title; + } + + ShuffleboardRoot getRoot() { + return m_root; + } + + @Override + public List> getComponents() { + return m_helper.getComponents(); + } + + @Override + public ShuffleboardLayout getLayout(String type, String title) { + return m_helper.getLayout(type, title); + } + + @Override + public ComplexWidget add(String title, Sendable sendable) { + return m_helper.add(title, sendable); + } + + @Override + public ComplexWidget add(Sendable sendable) throws IllegalArgumentException { + return m_helper.add(sendable); + } + + @Override + public SimpleWidget add(String title, Object defaultValue) { + return m_helper.add(title, defaultValue); + } + + @Override + public void buildInto(NetworkTable parentTable, NetworkTable metaTable) { + NetworkTable tabTable = parentTable.getSubTable(m_title); + tabTable.getEntry(".type").setString("ShuffleboardTab"); + for (ShuffleboardComponent component : m_helper.getComponents()) { + component.buildInto(tabTable, metaTable.getSubTable(component.getTitle())); + } + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java new file mode 100644 index 0000000000..8bfa6c37d1 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java @@ -0,0 +1,31 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import edu.wpi.first.networktables.NetworkTable; + +interface ShuffleboardValue { + + /** + * Gets the title of this Shuffleboard value. + */ + String getTitle(); + + /** + * Builds the entries for this value. + * + * @param parentTable the table containing all the data for the parent. Values that require a + * complex entry or table structure should call {@code + * parentTable.getSubTable(getTitle())} to get the table to put data into. + * Values that only use a single entry should call {@code + * parentTable.getEntry(getTitle())} to get that entry. + * @param metaTable the table containing all the metadata for this value and its sub-values + */ + void buildInto(NetworkTable parentTable, NetworkTable metaTable); + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java new file mode 100644 index 0000000000..91e532d817 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java @@ -0,0 +1,36 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +/** + * Abstract superclass for widgets. + * + *

This class is package-private to minimize API surface area. + * + * @param the self type + */ +abstract class ShuffleboardWidget> + extends ShuffleboardComponent { + + ShuffleboardWidget(ShuffleboardContainer parent, String title) { + super(parent, title); + } + + /** + * Sets the type of widget used to display the data. If not set, the default widget type will be + * used. + * + * @param widgetType the type of the widget used to display the data + * @return this widget object + */ + public final W withWidget(String widgetType) { + setType(widgetType); + return (W) this; + } + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java new file mode 100644 index 0000000000..c645baa4dd --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java @@ -0,0 +1,50 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableEntry; + +/** + * A Shuffleboard widget that handles a single data point such as a number or string. + */ +public final class SimpleWidget extends ShuffleboardWidget { + private NetworkTableEntry m_entry; + + SimpleWidget(ShuffleboardContainer parent, String title) { + super(parent, title); + } + + /** + * Gets the NetworkTable entry that contains the data for this widget. + */ + public NetworkTableEntry getEntry() { + if (m_entry == null) { + forceGenerate(); + } + return m_entry; + } + + @Override + public void buildInto(NetworkTable parentTable, NetworkTable metaTable) { + buildMetadata(metaTable); + if (m_entry == null) { + m_entry = parentTable.getEntry(getTitle()); + } + } + + private void forceGenerate() { + ShuffleboardContainer parent = getParent(); + while (parent instanceof ShuffleboardLayout) { + parent = ((ShuffleboardLayout) parent).getParent(); + } + ShuffleboardTab tab = (ShuffleboardTab) parent; + tab.getRoot().update(); + } + +} diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java new file mode 100644 index 0000000000..6cbeae2cf9 --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java @@ -0,0 +1,26 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import edu.wpi.first.wpilibj.SendableBase; +import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder; + +/** + * A mock sendable that marks itself as an actuator. + */ +public class MockActuatorSendable extends SendableBase { + public MockActuatorSendable(String name) { + super(false); + setName(name); + } + + @Override + public void initSendable(SendableBuilder builder) { + builder.setActuator(true); + } +} diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java new file mode 100644 index 0000000000..dea8ed772a --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java @@ -0,0 +1,115 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.NetworkTableInstance; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ShuffleboardInstanceTest { + private NetworkTableInstance m_ntInstance; + private ShuffleboardInstance m_shuffleboardInstance; + + @BeforeEach + void setupInstance() { + m_ntInstance = NetworkTableInstance.create(); + m_shuffleboardInstance = new ShuffleboardInstance(m_ntInstance); + } + + @AfterEach + void tearDownInstance() { + m_ntInstance.close(); + } + + @Test + void testPathFluent() { + NetworkTableEntry entry = m_shuffleboardInstance.getTab("Tab Title") + .getLayout("List", "List Layout") + .add("Data", "string") + .withWidget("Text View") + .getEntry(); + + assertAll( + () -> assertEquals("string", entry.getString(null), "Wrong entry value"), + () -> assertEquals("/Shuffleboard/Tab Title/List Layout/Data", entry.getName(), + "Entry path generated incorrectly")); + } + + @Test + void testNestedLayoutsFluent() { + NetworkTableEntry entry = m_shuffleboardInstance.getTab("Tab") + .getLayout("List", "First") + .getLayout("List", "Second") + .getLayout("List", "Third") + .getLayout("List", "Fourth") + .add("Value", "string") + .getEntry(); + + assertAll( + () -> assertEquals("string", entry.getString(null), "Wrong entry value"), + () -> assertEquals("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", entry.getName(), + "Entry path generated incorrectly")); + } + + @Test + void testNestedLayoutsOop() { + ShuffleboardTab tab = m_shuffleboardInstance.getTab("Tab"); + ShuffleboardLayout first = tab.getLayout("List", "First"); + ShuffleboardLayout second = first.getLayout("List", "Second"); + ShuffleboardLayout third = second.getLayout("List", "Third"); + ShuffleboardLayout fourth = third.getLayout("List", "Fourth"); + SimpleWidget widget = fourth.add("Value", "string"); + NetworkTableEntry entry = widget.getEntry(); + + assertAll( + () -> assertEquals("string", entry.getString(null), "Wrong entry value"), + () -> assertEquals("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", entry.getName(), + "Entry path generated incorrectly")); + } + + @Test + void testLayoutTypeIsSet() { + String layoutType = "Type"; + m_shuffleboardInstance.getTab("Tab") + .getLayout(layoutType, "Title"); + m_shuffleboardInstance.update(); + NetworkTableEntry entry = m_ntInstance.getEntry( + "/Shuffleboard/.metadata/Tab/Title/PreferredComponent"); + assertEquals(layoutType, entry.getString("Not Set"), "Layout type not set"); + } + + @Test + void testNestedActuatorWidgetsAreDisabled() { + m_shuffleboardInstance.getTab("Tab") + .getLayout("Layout", "Title") + .add(new MockActuatorSendable("Actuator")); + NetworkTableEntry controllableEntry = + m_ntInstance.getEntry("/Shuffleboard/Tab/Title/Actuator/.controllable"); + + m_shuffleboardInstance.update(); + + // Note: we use the unsafe `getBoolean()` method because if the value is NOT a boolean, or if it + // is not present, then something has clearly gone very, very wrong + boolean controllable = controllableEntry.getValue().getBoolean(); + + // Sanity check + assertTrue(controllable, "The nested actuator widget should be enabled by default"); + m_shuffleboardInstance.disableActuatorWidgets(); + controllable = controllableEntry.getValue().getBoolean(); + assertFalse(controllable, "The nested actuator widget should have been disabled"); + } + +} diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTabTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTabTest.java new file mode 100644 index 0000000000..fe48c130ef --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTabTest.java @@ -0,0 +1,152 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.wpilibj.Sendable; +import edu.wpi.first.wpilibj.command.InstantCommand; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SuppressWarnings({"PMD.TooManyMethods"}) +public class ShuffleboardTabTest { + private NetworkTableInstance m_ntInstance; + private ShuffleboardTab m_tab; + private ShuffleboardInstance m_instance; + + @BeforeEach + void setup() { + m_ntInstance = NetworkTableInstance.create(); + m_instance = new ShuffleboardInstance(m_ntInstance); + m_tab = m_instance.getTab("Tab"); + } + + @AfterEach + void tearDown() { + m_ntInstance.close(); + } + + @Test + void testAddDouble() { + NetworkTableEntry entry = m_tab.add("Double", 1.0).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/Double", entry.getName()), + () -> assertEquals(1.0, entry.getValue().getDouble())); + } + + @Test + void testAddInteger() { + NetworkTableEntry entry = m_tab.add("Int", 1).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/Int", entry.getName()), + () -> assertEquals(1.0, entry.getValue().getDouble())); + } + + @Test + void testAddLong() { + NetworkTableEntry entry = m_tab.add("Long", 1L).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/Long", entry.getName()), + () -> assertEquals(1.0, entry.getValue().getDouble())); + } + + + @Test + void testAddBoolean() { + NetworkTableEntry entry = m_tab.add("Bool", false).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/Bool", entry.getName()), + () -> assertFalse(entry.getValue().getBoolean())); + } + + @Test + void testAddString() { + NetworkTableEntry entry = m_tab.add("String", "foobar").getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/String", entry.getName()), + () -> assertEquals("foobar", entry.getValue().getString())); + } + + @Test + void testAddNamedSendableWithProperties() { + Sendable sendable = new InstantCommand("Command"); + String widgetType = "Command Widget"; + m_tab.add(sendable) + .withWidget(widgetType) + .withProperties(mapOf("foo", 1234, "bar", "baz")); + + m_instance.update(); + String meta = "/Shuffleboard/.metadata/Tab/Command"; + + assertAll( + () -> assertEquals(1234, + m_ntInstance.getEntry(meta + "/Properties/foo").getDouble(-1), + "Property 'foo' not set correctly"), + () -> assertEquals("baz", + m_ntInstance.getEntry(meta + "/Properties/bar").getString(null), + "Property 'bar' not set correctly"), + () -> assertEquals(widgetType, + m_ntInstance.getEntry(meta + "/PreferredComponent").getString(null), + "Preferred component not set correctly")); + } + + @Test + void testAddNumberArray() { + NetworkTableEntry entry = m_tab.add("DoubleArray", new double[]{1, 2, 3}).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/DoubleArray", entry.getName()), + () -> assertArrayEquals(new double[]{1, 2, 3}, entry.getValue().getDoubleArray())); + } + + @Test + void testAddBooleanArray() { + NetworkTableEntry entry = m_tab.add("BoolArray", new boolean[]{true, false}).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/BoolArray", entry.getName()), + () -> assertArrayEquals(new boolean[]{true, false}, entry.getValue().getBooleanArray())); + } + + @Test + void testAddStringArray() { + NetworkTableEntry entry = m_tab.add("StringArray", new String[]{"foo", "bar"}).getEntry(); + assertAll( + () -> assertEquals("/Shuffleboard/Tab/StringArray", entry.getName()), + () -> assertArrayEquals(new String[]{"foo", "bar"}, entry.getValue().getStringArray())); + } + + @Test + void testTitleDuplicates() { + m_tab.add("foo", "bar"); + assertThrows(IllegalArgumentException.class, () -> m_tab.add("foo", "baz")); + } + + /** + * Stub for Java 9 {@code Map.of()}. + */ + @SuppressWarnings({"unchecked", "PMD"}) + private static Map mapOf(Object... entries) { + Map map = new HashMap<>(); + for (int i = 0; i < entries.length; i += 2) { + map.put((K) entries[i], (V) entries[i + 1]); + } + return map; + } + +} diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java new file mode 100644 index 0000000000..c4285fb099 --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java @@ -0,0 +1,30 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.shuffleboard; + +import org.junit.jupiter.api.Test; + +import edu.wpi.first.wpilibj.UtilityClassTest; + +import static org.junit.jupiter.api.Assertions.assertSame; + +public class ShuffleboardTest extends UtilityClassTest { + public ShuffleboardTest() { + super(Shuffleboard.class); + } + + // Most relevant tests are in ShuffleboardTabTest + + @Test + void testTabObjectsCached() { + ShuffleboardTab tab1 = Shuffleboard.getTab("testTabObjectsCached"); + ShuffleboardTab tab2 = Shuffleboard.getTab("testTabObjectsCached"); + assertSame(tab1, tab2, "Tab objects were not cached"); + } + +} diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/examples.json b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/examples.json index eb2a26ed93..19af701cd1 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/examples.json +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/examples.json @@ -176,5 +176,16 @@ "foldername": "axiscamera", "gradlebase": "java", "mainclass": "Main" + }, + { + "name": "Shuffleboard Sample", + "description": "An example program that adds data to various Shuffleboard tabs that demonstrates the Shuffleboard API", + "tags": [ + "Shuffleboard", + "Dashboards" + ], + "foldername": "shuffleboard", + "gradlebase": "java", + "mainclass": "Main" } ] diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Main.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Main.java new file mode 100644 index 0000000000..d2283df467 --- /dev/null +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Main.java @@ -0,0 +1,29 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.examples.shuffleboard; + +import edu.wpi.first.wpilibj.RobotBase; + +/** + * Do NOT add any static variables to this class, or any initialization at all. + * Unless you know what you are doing, do not modify this file except to + * change the parameter class to the startRobot call. + */ +public final class Main { + private Main() { + } + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java new file mode 100644 index 0000000000..a04a860933 --- /dev/null +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.examples.shuffleboard; + +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.wpilibj.AnalogPotentiometer; +import edu.wpi.first.wpilibj.Encoder; +import edu.wpi.first.wpilibj.IterativeRobot; +import edu.wpi.first.wpilibj.Spark; +import edu.wpi.first.wpilibj.drive.DifferentialDrive; +import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; +import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardLayout; +import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardTab; + +public class Robot extends IterativeRobot { + private final DifferentialDrive m_tankDrive = new DifferentialDrive(new Spark(0), new Spark(1)); + private final Encoder m_leftEncoder = new Encoder(0, 1); + private final Encoder m_rightEncoder = new Encoder(2, 3); + + private final Spark m_elevatorMotor = new Spark(2); + private final AnalogPotentiometer m_elevatorPot = new AnalogPotentiometer(0); + private NetworkTableEntry m_maxSpeed; + + @Override + public void robotInit() { + // Add a 'max speed' widget to a tab named 'Configuration', using a number slider + // The widget will be placed in the second column and row and will be two columns wide + m_maxSpeed = Shuffleboard.getTab("Configuration") + .add("Max Speed", 1) + .withWidget("Number Slider") + .withPosition(1, 1) + .withSize(2, 1) + .getEntry(); + + // Add the tank drive and encoders to a 'Drivebase' tab + ShuffleboardTab driveBaseTab = Shuffleboard.getTab("Drivebase"); + driveBaseTab.add("Tank Drive", m_tankDrive); + // Put both encoders in a list layout + ShuffleboardLayout encoders = driveBaseTab.getLayout("List", "Encoders") + .withPosition(0, 0) + .withSize(2, 2); + encoders.add("Left Encoder", m_leftEncoder); + encoders.add("Right Encoder", m_rightEncoder); + + // Add the elevator motor and potentiometer to an 'Elevator' tab + ShuffleboardTab elevatorTab = Shuffleboard.getTab("Elevator"); + elevatorTab.add("Motor", m_elevatorMotor); + elevatorTab.add("Potentiometer", m_elevatorPot); + } + + @Override + public void autonomousInit() { + // Read the value of the 'max speed' widget from the dashboard + m_tankDrive.setMaxOutput(m_maxSpeed.getDouble(1.0)); + } + +}