diff --git a/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp b/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp new file mode 100644 index 0000000000..294be7979e --- /dev/null +++ b/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp @@ -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. */ +/*----------------------------------------------------------------------------*/ + +#include "frc/shuffleboard/RecordingController.h" + +#include "frc/DriverStation.h" + +using namespace frc; +using namespace frc::detail; + +RecordingController::RecordingController(nt::NetworkTableInstance ntInstance) + : m_recordingControlEntry(), m_recordingFileNameFormatEntry() { + m_recordingControlEntry = + ntInstance.GetEntry("/Shuffleboard/.recording/RecordData"); + m_recordingFileNameFormatEntry = + ntInstance.GetEntry("/Shuffleboard/.recording/FileNameFormat"); + m_eventsTable = ntInstance.GetTable("/Shuffleboard/.recording/events"); +} + +void RecordingController::StartRecording() { + m_recordingControlEntry.SetBoolean(true); +} + +void RecordingController::StopRecording() { + m_recordingControlEntry.SetBoolean(false); +} + +void RecordingController::SetRecordingFileNameFormat(wpi::StringRef format) { + m_recordingFileNameFormatEntry.SetString(format); +} + +void RecordingController::ClearRecordingFileNameFormat() { + m_recordingFileNameFormatEntry.Delete(); +} + +void RecordingController::AddEventMarker( + wpi::StringRef name, wpi::StringRef description, + ShuffleboardEventImportance importance) { + if (name.empty()) { + DriverStation::ReportError("Shuffleboard event name was not specified"); + return; + } + auto arr = wpi::ArrayRef{ + description, ShuffleboardEventImportanceName(importance)}; + m_eventsTable->GetSubTable(name)->GetEntry("Info").SetStringArray(arr); +} diff --git a/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp b/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp index ec0111619a..267ef36f25 100644 --- a/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp +++ b/wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp @@ -29,8 +29,39 @@ void Shuffleboard::DisableActuatorWidgets() { GetInstance().DisableActuatorWidgets(); } +void Shuffleboard::StartRecording() { + GetRecordingController().StartRecording(); +} + +void Shuffleboard::StopRecording() { GetRecordingController().StopRecording(); } + +void Shuffleboard::SetRecordingFileNameFormat(wpi::StringRef format) { + GetRecordingController().SetRecordingFileNameFormat(format); +} + +void Shuffleboard::ClearRecordingFileNameFormat() { + GetRecordingController().ClearRecordingFileNameFormat(); +} + +void Shuffleboard::AddEventMarker(wpi::StringRef name, + wpi::StringRef description, + ShuffleboardEventImportance importance) { + GetRecordingController().AddEventMarker(name, description, importance); +} + +void Shuffleboard::AddEventMarker(wpi::StringRef name, + ShuffleboardEventImportance importance) { + AddEventMarker(name, "", importance); +} + detail::ShuffleboardInstance& Shuffleboard::GetInstance() { static detail::ShuffleboardInstance inst( nt::NetworkTableInstance::GetDefault()); return inst; } + +detail::RecordingController& Shuffleboard::GetRecordingController() { + static detail::RecordingController inst( + nt::NetworkTableInstance::GetDefault()); + return inst; +} diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h b/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h new file mode 100644 index 0000000000..dacbdb47c9 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h @@ -0,0 +1,43 @@ +/*----------------------------------------------------------------------------*/ +/* 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/ShuffleboardEventImportance.h" + +namespace frc { +namespace detail { + +class RecordingController final { + public: + explicit RecordingController(nt::NetworkTableInstance ntInstance); + virtual ~RecordingController() = default; + + void StartRecording(); + void StopRecording(); + void SetRecordingFileNameFormat(wpi::StringRef format); + void ClearRecordingFileNameFormat(); + + void AddEventMarker(wpi::StringRef name, wpi::StringRef description, + ShuffleboardEventImportance importance); + + private: + nt::NetworkTableEntry m_recordingControlEntry; + nt::NetworkTableEntry m_recordingFileNameFormatEntry; + std::shared_ptr m_eventsTable; +}; + +} // namespace detail +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h b/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h index 383a6654de..a60ed729e5 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/Shuffleboard.h @@ -9,6 +9,8 @@ #include +#include "frc/shuffleboard/RecordingController.h" +#include "frc/shuffleboard/ShuffleboardEventImportance.h" #include "frc/shuffleboard/ShuffleboardInstance.h" namespace frc { @@ -97,8 +99,77 @@ class Shuffleboard final { */ static void DisableActuatorWidgets(); + /** + * Starts data recording on the dashboard. Has no effect if recording is + * already in progress. + */ + static void StartRecording(); + + /** + * Stops data recording on the dashboard. Has no effect if no recording is in + * progress. + */ + static void StopRecording(); + + /** + * Sets the file name format for new recording files to use. If recording is + * in progress when this method is called, it will continue to use the same + * file. New recordings will use the format. + * + *

To avoid recording files overwriting each other, make sure to use unique + * recording file names. File name formats accept templates for inserting the + * date and time when the recording started with the {@code ${date}} and + * {@code ${time}} templates, respectively. For example, the default format is + * {@code "recording-${time}"} and recording files created with it will have + * names like {@code "recording-2018.01.15.sbr"}. Users are + * strongly recommended to use the {@code ${time}} template + * to ensure unique file names. + *

+ * + * @param format the format for the + */ + static void SetRecordingFileNameFormat(wpi::StringRef format); + + /** + * Clears the custom name format for recording files. New recordings will use + * the default format. + * + * @see #setRecordingFileNameFormat(String) + */ + static void ClearRecordingFileNameFormat(); + + /** + * Notifies Shuffleboard of an event. Events can range from as trivial as a + * change in a command state to as critical as a total power loss or component + * failure. If Shuffleboard is recording, the event will also be recorded. + * + *

If {@code name} is {@code null} or empty, no event will be sent and an + * error will be printed to the driver station. + * + * @param name the name of the event + * @param description a description of the event + * @param importance the importance of the event + */ + static void AddEventMarker(wpi::StringRef name, wpi::StringRef description, + ShuffleboardEventImportance importance); + + /** + * Notifies Shuffleboard of an event. Events can range from as trivial as a + * change in a command state to as critical as a total power loss or component + * failure. If Shuffleboard is recording, the event will also be recorded. + * + *

If {@code name} is {@code null} or empty, no event will be sent and an + * error will be printed to the driver station. + * + * @param name the name of the event + * @param importance the importance of the event + */ + static void AddEventMarker(wpi::StringRef name, + ShuffleboardEventImportance importance); + private: static detail::ShuffleboardInstance& GetInstance(); + static detail::RecordingController& GetRecordingController(); // TODO usage reporting diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardEventImportance.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardEventImportance.h new file mode 100644 index 0000000000..ccbc80259f --- /dev/null +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardEventImportance.h @@ -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. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +namespace frc { + +// Maintainer note: this enum is mirrored in WPILibJ and in Shuffleboard +// Modifying the enum or enum strings requires a corresponding change to the +// Java enum and the enum in Shuffleboard + +enum ShuffleboardEventImportance { kTrivial, kLow, kNormal, kHigh, kCritical }; + +inline wpi::StringRef ShuffleboardEventImportanceName( + ShuffleboardEventImportance importance) { + switch (importance) { + case kTrivial: + return "TRIVIAL"; + case kLow: + return "LOW"; + case kNormal: + return "NORMAL"; + case kHigh: + return "HIGH"; + case kCritical: + return "CRITICAL"; + default: + return "NORMAL"; + } +} + +} // namespace frc diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java new file mode 100644 index 0000000000..08f9712fdb --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java @@ -0,0 +1,55 @@ +/*----------------------------------------------------------------------------*/ +/* 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 importance of an event marker in Shuffleboard. The exact meaning of each importance level is + * up for interpretation on a team-to-team basis, but users should follow the general guidelines + * of the various importance levels. The examples given are for reference and may be ignored or + * considered to be more or less important from team to team. + */ +public enum EventImportance { + // Maintainer note: this enum is mirrored in WPILibC and in Shuffleboard + // Modifying the enum or enum strings requires a corresponding change to the C++ enum + // and the enum in Shuffleboard + + /** + * A trivial event such as a change in command state. + */ + kTrivial("TRIVIAL"), + + /** + * A low importance event such as acquisition of a game piece. + */ + kLow("LOW"), + + /** + * A "normal" importance event, such as a transition from autonomous mode to teleoperated control. + */ + kNormal("NORMAL"), + + /** + * A high-importance event such as scoring a game piece. + */ + kHigh("HIGH"), + + /** + * A critically important event such as a brownout, component failure, or software deadlock. + */ + kCritical("CRITICAL"); + + private final String m_simpleName; + + EventImportance(String simpleName) { + m_simpleName = simpleName; + } + + public String getSimpleName() { + return m_simpleName; + } +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java new file mode 100644 index 0000000000..dbbdc383a5 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java @@ -0,0 +1,69 @@ +/*----------------------------------------------------------------------------*/ +/* 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; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.wpilibj.DriverStation; + +/** + * Controls Shuffleboard recordings via NetworkTables. + */ +final class RecordingController { + private static final String kRecordingTableName = "/Shuffleboard/.recording/"; + private static final String kRecordingControlKey = kRecordingTableName + "RecordData"; + private static final String kRecordingFileNameFormatKey = kRecordingTableName + "FileNameFormat"; + private static final String kEventMarkerTableName = kRecordingTableName + "events"; + + private final NetworkTableEntry m_recordingControlEntry; + private final NetworkTableEntry m_recordingFileNameFormatEntry; + private final NetworkTable m_eventsTable; + + RecordingController(NetworkTableInstance ntInstance) { + m_recordingControlEntry = ntInstance.getEntry(kRecordingControlKey); + m_recordingFileNameFormatEntry = ntInstance.getEntry(kRecordingFileNameFormatKey); + m_eventsTable = ntInstance.getTable(kEventMarkerTableName); + } + + public void startRecording() { + m_recordingControlEntry.setBoolean(true); + } + + public void stopRecording() { + m_recordingControlEntry.setBoolean(false); + } + + public void setRecordingFileNameFormat(String format) { + m_recordingFileNameFormatEntry.setString(format); + } + + public void clearRecordingFileNameFormat() { + m_recordingFileNameFormatEntry.delete(); + } + + public void addEventMarker(String name, String description, EventImportance importance) { + if (name == null || name.isEmpty() || name.isBlank()) { + DriverStation.reportError( + "Shuffleboard event name was not specified", true); + return; + } + + if (importance == null) { + DriverStation.reportError( + "Shuffleboard event importance was null", true); + return; + } + + String eventDescription = description == null || description.isBlank() ? "" : description; + + m_eventsTable.getSubTable(name) + .getEntry("Info") + .setStringArray(new String[]{eventDescription, importance.getSimpleName()}); + } +} 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 index be742c7003..dbfd25e7fb 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java @@ -54,6 +54,8 @@ public final class Shuffleboard { private static final ShuffleboardRoot root = new ShuffleboardInstance(NetworkTableInstance.getDefault()); + private static final RecordingController recordingController = + new RecordingController(NetworkTableInstance.getDefault()); // TODO usage reporting @@ -101,4 +103,80 @@ public final class Shuffleboard { root.disableActuatorWidgets(); } + /** + * Starts data recording on the dashboard. Has no effect if recording is already in progress. + * + * @see #stopRecording() + */ + public static void startRecording() { + recordingController.startRecording(); + } + + /** + * Stops data recording on the dashboard. Has no effect if no recording is in progress. + * + * @see #startRecording() + */ + public static void stopRecording() { + recordingController.stopRecording(); + } + + /** + * Sets the file name format for new recording files to use. If recording is in progress when this + * method is called, it will continue to use the same file. New recordings will use the format. + * + *

To avoid recording files overwriting each other, make sure to use unique recording file + * names. File name formats accept templates for inserting the date and time when the recording + * started with the {@code ${date}} and {@code ${time}} templates, respectively. For example, + * the default format is {@code "recording-${time}"} and recording files created with it will have + * names like {@code "recording-2018.01.15.sbr"}. Users are strongly recommended + * to use the {@code ${time}} template to ensure unique file names. + *

+ * + * @param format the format for the + * @see #clearRecordingFileNameFormat() + */ + public static void setRecordingFileNameFormat(String format) { + recordingController.setRecordingFileNameFormat(format); + } + + /** + * Clears the custom name format for recording files. New recordings will use the default format. + * + * @see #setRecordingFileNameFormat(String) + */ + public static void clearRecordingFileNameFormat() { + recordingController.clearRecordingFileNameFormat(); + } + + /** + * Notifies Shuffleboard of an event. Events can range from as trivial as a change in a command + * state to as critical as a total power loss or component failure. If Shuffleboard is recording, + * the event will also be recorded. + * + *

If {@code name} is {@code null} or empty, or {@code importance} is {@code null}, then + * no event will be sent and an error will be printed to the driver station. + * + * @param name the name of the event + * @param description a description of the event + * @param importance the importance of the event + */ + public static void addEventMarker(String name, String description, EventImportance importance) { + recordingController.addEventMarker(name, description, importance); + } + + /** + * Notifies Shuffleboard of an event. Events can range from as trivial as a change in a command + * state to as critical as a total power loss or component failure. If Shuffleboard is recording, + * the event will also be recorded. + * + *

If {@code name} is {@code null} or empty, or {@code importance} is {@code null}, then + * no event will be sent and an error will be printed to the driver station. + * + * @param name the name of the event + * @param importance the importance of the event + */ + public static void addEventMarker(String name, EventImportance importance) { + addEventMarker(name, "", importance); + } }