diff --git a/commandsv2/src/main/java/org/wpilib/command2/WaitUntilCommand.java b/commandsv2/src/main/java/org/wpilib/command2/WaitUntilCommand.java
index 53711ea273..745936e3fb 100644
--- a/commandsv2/src/main/java/org/wpilib/command2/WaitUntilCommand.java
+++ b/commandsv2/src/main/java/org/wpilib/command2/WaitUntilCommand.java
@@ -34,10 +34,15 @@ public class WaitUntilCommand extends Command {
* guarantee that the time at which the action is performed will be judged to be legal by the
* referees. When in doubt, add a safety factor or time the action manually.
*
+ *
The match time counts down when connected to FMS or the DS is in practice mode for the
+ * current mode. When the DS is not connected to FMS or in practice mode, the command will not
+ * wait.
+ *
* @param time the match time after which to end, in seconds
+ * @see edu.wpi.first.wpilibj.DriverStation#getMatchTime()
*/
public WaitUntilCommand(double time) {
- this(() -> Timer.getMatchTime() - time > 0);
+ this(() -> Timer.getMatchTime() < time);
}
@Override
diff --git a/commandsv2/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp b/commandsv2/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp
index 3fe635535b..1074fce8f8 100644
--- a/commandsv2/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp
+++ b/commandsv2/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp
@@ -14,7 +14,7 @@ WaitUntilCommand::WaitUntilCommand(std::function condition)
: m_condition{std::move(condition)} {}
WaitUntilCommand::WaitUntilCommand(wpi::units::second_t time)
- : m_condition{[=] { return wpi::Timer::GetMatchTime() - time > 0_s; }} {}
+ : m_condition{[=] { return wpi::Timer::GetMatchTime() < time; }} {}
bool WaitUntilCommand::IsFinished() {
return m_condition();
diff --git a/commandsv2/src/main/native/include/wpi/commands2/WaitUntilCommand.hpp b/commandsv2/src/main/native/include/wpi/commands2/WaitUntilCommand.hpp
index 334751b2fe..5039fb2825 100644
--- a/commandsv2/src/main/native/include/wpi/commands2/WaitUntilCommand.hpp
+++ b/commandsv2/src/main/native/include/wpi/commands2/WaitUntilCommand.hpp
@@ -35,7 +35,12 @@ class WaitUntilCommand : public CommandHelper {
* will be judged to be legal by the referees. When in doubt, add a safety
* factor or time the action manually.
*
+ * The match time counts down when connected to FMS or the DS is in practice
+ * mode for the current mode. When the DS is not connected to FMS or in
+ * practice mode, the command will not wait.
+ *
* @param time the match time after which to end, in seconds
+ * @see frc::DriverStation::GetMatchTime()
*/
explicit WaitUntilCommand(wpi::units::second_t time);
diff --git a/docs/build.gradle b/docs/build.gradle
index 4f2b09800b..28a5bb1c96 100644
--- a/docs/build.gradle
+++ b/docs/build.gradle
@@ -159,7 +159,9 @@ configurations {
task generateJavaDocs(type: Javadoc) {
classpath += project(":wpilibj").sourceSets.main.compileClasspath
options.links("https://docs.oracle.com/en/java/javase/21/docs/api/")
- options.links("https://docs.opencv.org/4.x/javadoc/")
+ // workaround for opencv site blocking javadoc tool. If the link is changed,
+ // docs/opencv/element-list must be redownloaded
+ options.linksOffline("https://docs.opencv.org/4.10.0/javadoc/", "opencv")
options.addStringOption("tag", "pre:a:Pre-Condition")
options.addBooleanOption("Xdoclint/package:" +
// TODO: v Document these, then remove them from the list
diff --git a/docs/opencv/element-list b/docs/opencv/element-list
new file mode 100644
index 0000000000..270c38bf6b
--- /dev/null
+++ b/docs/opencv/element-list
@@ -0,0 +1,29 @@
+org.opencv.aruco
+org.opencv.bgsegm
+org.opencv.bioinspired
+org.opencv.calib3d
+org.opencv.core
+org.opencv.dnn
+org.opencv.dnn_superres
+org.opencv.face
+org.opencv.features2d
+org.opencv.highgui
+org.opencv.img_hash
+org.opencv.imgcodecs
+org.opencv.imgproc
+org.opencv.ml
+org.opencv.objdetect
+org.opencv.osgi
+org.opencv.phase_unwrapping
+org.opencv.photo
+org.opencv.plot
+org.opencv.structured_light
+org.opencv.text
+org.opencv.tracking
+org.opencv.utils
+org.opencv.video
+org.opencv.videoio
+org.opencv.wechat_qrcode
+org.opencv.xfeatures2d
+org.opencv.ximgproc
+org.opencv.xphoto
diff --git a/ntcore/src/main/java/org/wpilib/networktables/Topic.java b/ntcore/src/main/java/org/wpilib/networktables/Topic.java
index 268ab494d6..1e97daeb23 100644
--- a/ntcore/src/main/java/org/wpilib/networktables/Topic.java
+++ b/ntcore/src/main/java/org/wpilib/networktables/Topic.java
@@ -330,8 +330,8 @@ public class Topic {
}
/** NetworkTables instance. */
- protected NetworkTableInstance m_inst;
+ protected final NetworkTableInstance m_inst;
/** NetworkTables handle. */
- protected int m_handle;
+ protected final int m_handle;
}
diff --git a/ntcore/src/main/native/cpp/NetworkServer.cpp b/ntcore/src/main/native/cpp/NetworkServer.cpp
index c01968e2ff..0001cf4d4e 100644
--- a/ntcore/src/main/native/cpp/NetworkServer.cpp
+++ b/ntcore/src/main/native/cpp/NetworkServer.cpp
@@ -302,6 +302,21 @@ void NetworkServer::ProcessAllLocal() {
}
void NetworkServer::LoadPersistent() {
+ // check if SavePersistent was interrupted and left a backup file;
+ // if so, try to restore it
+ auto bak = fmt::format("{}.bck", m_persistentFilename);
+ if (!fs::exists(m_persistentFilename) && fs::exists(bak)) {
+ INFO(
+ "restoring persistent file from backup '{}', since original '{}' is "
+ "missing",
+ bak, m_persistentFilename);
+ std::error_code ec;
+ fs::rename(bak, m_persistentFilename, ec);
+ if (ec.value() != 0) {
+ INFO("failed to restore backup: {}", ec.message());
+ }
+ }
+
auto fileBuffer = wpi::util::MemoryBuffer::GetFile(m_persistentFilename);
if (!fileBuffer) {
INFO(
diff --git a/ntcore/src/test/native/cpp/NetworkServerTest.cpp b/ntcore/src/test/native/cpp/NetworkServerTest.cpp
new file mode 100644
index 0000000000..0424230532
--- /dev/null
+++ b/ntcore/src/test/native/cpp/NetworkServerTest.cpp
@@ -0,0 +1,179 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "wpi/nt/IntegerTopic.hpp"
+#include "wpi/nt/NetworkTableInstance.hpp"
+
+// Valid persistent JSON containing a single persistent integer topic.
+static constexpr const char* kPersistentJson = R"([
+ {
+ "name": "/test/persistent_value",
+ "type": "int",
+ "value": 42,
+ "properties": {"persistent": true}
+ }
+])";
+
+class NetworkServerPersistentTest : public ::testing::Test {
+ public:
+ NetworkServerPersistentTest() {
+ // Create a unique temp directory for each test
+ m_tempDir =
+ std::filesystem::temp_directory_path() /
+ ("ntcore_test_" +
+ std::to_string(
+ std::chrono::steady_clock::now().time_since_epoch().count()));
+ std::filesystem::create_directories(m_tempDir);
+ m_persistFile = (m_tempDir / "test_persistent.json").string();
+ }
+
+ ~NetworkServerPersistentTest() override {
+ std::error_code ec;
+ std::filesystem::remove_all(m_tempDir, ec);
+ }
+
+ protected:
+ // Write content to a file.
+ static void WriteFile(const std::string& path, const std::string& content) {
+ std::ofstream os{path};
+ ASSERT_TRUE(os.is_open()) << "Failed to create file: " << path;
+ os << content;
+ }
+
+ // Wait for the server to finish initializing. Returns true if a topic with
+ // the given name was seen before the timeout expired.
+ bool WaitForTopic(
+ wpi::nt::NetworkTableInstance& inst, std::string_view name,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds{3000}) {
+ auto deadline = std::chrono::steady_clock::now() + timeout;
+ while (std::chrono::steady_clock::now() < deadline) {
+ auto infos = inst.GetTopicInfo(name);
+ if (!infos.empty()) {
+ return true;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds{50});
+ }
+ return false;
+ }
+
+ std::filesystem::path m_tempDir;
+ std::string m_persistFile;
+};
+
+// Verify that LoadPersistent restores from the .bck backup file when the
+// original persistent file is missing. This simulates SavePersistent being
+// interrupted after renaming the original file to .bck but before the
+// temporary file has been renamed to the original filename.
+TEST_F(NetworkServerPersistentTest,
+ LoadPersistentRestoresFromBackupWhenOriginalMissing) {
+ // Set up "interrupted" state: only .bck file exists, no original.
+ std::string backupFile = m_persistFile + ".bck";
+ WriteFile(backupFile, kPersistentJson);
+ ASSERT_TRUE(std::filesystem::exists(backupFile));
+ ASSERT_FALSE(std::filesystem::exists(m_persistFile));
+
+ // Start a server that references the (missing) persistent file.
+ // Subscribe BEFORE starting the server so the server's local client has a
+ // matching subscriber when persistent topics are announced.
+ auto inst = wpi::nt::NetworkTableInstance::Create();
+ wpi::nt::IntegerSubscriber sub =
+ inst.GetIntegerTopic("/test/persistent_value").Subscribe(0);
+ inst.StartServer(m_persistFile, "127.0.0.1");
+
+ // Wait for the persistent topic to appear.
+ EXPECT_TRUE(WaitForTopic(inst, "/test/persistent_value"))
+ << "LoadPersistent did not restore from the .bck backup file";
+
+ // Also verify the value is correct.
+ EXPECT_EQ(sub.Get(), 42);
+
+ // The .bck should have been renamed to the original filename.
+ EXPECT_TRUE(std::filesystem::exists(m_persistFile));
+
+ inst.StopServer();
+ wpi::nt::NetworkTableInstance::Destroy(inst);
+}
+
+// Verify that LoadPersistent works normally when the original persistent file
+// is present (no interruption scenario).
+TEST_F(NetworkServerPersistentTest, LoadPersistentNormalLoad) {
+ // Write the persistent file directly (no backup).
+ WriteFile(m_persistFile, kPersistentJson);
+ ASSERT_TRUE(std::filesystem::exists(m_persistFile));
+
+ auto inst = wpi::nt::NetworkTableInstance::Create();
+ wpi::nt::IntegerSubscriber sub =
+ inst.GetIntegerTopic("/test/persistent_value").Subscribe(0);
+ inst.StartServer(m_persistFile, "127.0.0.1");
+
+ EXPECT_TRUE(WaitForTopic(inst, "/test/persistent_value"))
+ << "LoadPersistent did not load the persistent file";
+
+ EXPECT_EQ(sub.Get(), 42);
+
+ inst.StopServer();
+ wpi::nt::NetworkTableInstance::Destroy(inst);
+}
+
+// Verify that when both the original file and .bck exist, the original file
+// takes precedence (the backup is not used).
+TEST_F(NetworkServerPersistentTest, LoadPersistentPrefersOriginalOverBackup) {
+ // Original file with value 100.
+ static constexpr const char* kOriginalJson = R"([
+ {
+ "name": "/test/persistent_value",
+ "type": "int",
+ "value": 100,
+ "properties": {"persistent": true}
+ }
+])";
+
+ // Backup file with a different value (42).
+ WriteFile(m_persistFile, kOriginalJson);
+ WriteFile(m_persistFile + ".bck", kPersistentJson);
+ ASSERT_TRUE(std::filesystem::exists(m_persistFile));
+ ASSERT_TRUE(std::filesystem::exists(m_persistFile + ".bck"));
+
+ auto inst = wpi::nt::NetworkTableInstance::Create();
+ wpi::nt::IntegerSubscriber sub =
+ inst.GetIntegerTopic("/test/persistent_value").Subscribe(0);
+ inst.StartServer(m_persistFile, "127.0.0.1");
+
+ EXPECT_TRUE(WaitForTopic(inst, "/test/persistent_value"))
+ << "LoadPersistent did not load any persistent file";
+
+ // The value should come from the original (100), not the backup (42).
+ EXPECT_EQ(sub.Get(), 100);
+
+ inst.StopServer();
+ wpi::nt::NetworkTableInstance::Destroy(inst);
+}
+
+// Verify that LoadPersistent handles a missing persistent file and no backup
+// gracefully (no crash, no topics loaded).
+TEST_F(NetworkServerPersistentTest, LoadPersistentNoFile) {
+ ASSERT_FALSE(std::filesystem::exists(m_persistFile));
+ ASSERT_FALSE(std::filesystem::exists(m_persistFile + ".bck"));
+
+ auto inst = wpi::nt::NetworkTableInstance::Create();
+ inst.StartServer(m_persistFile, "127.0.0.1");
+
+ // Give the server time to initialize.
+ std::this_thread::sleep_for(std::chrono::milliseconds{500});
+
+ // No persistent topics should exist.
+ auto infos = inst.GetTopicInfo("/test/persistent_value");
+ EXPECT_TRUE(infos.empty());
+
+ inst.StopServer();
+ wpi::nt::NetworkTableInstance::Destroy(inst);
+}
diff --git a/wpilibc/src/main/native/include/wpi/simulation/SingleJointedArmSim.hpp b/wpilibc/src/main/native/include/wpi/simulation/SingleJointedArmSim.hpp
index 3e8e3d68cc..5283726891 100644
--- a/wpilibc/src/main/native/include/wpi/simulation/SingleJointedArmSim.hpp
+++ b/wpilibc/src/main/native/include/wpi/simulation/SingleJointedArmSim.hpp
@@ -28,8 +28,10 @@ class SingleJointedArmSim : public LinearSystemSim<2, 1, 2> {
* @param gearing The gear ratio of the arm (numbers greater than 1 represent
* reductions).
* @param armLength The length of the arm.
- * @param minAngle The minimum angle that the arm is capable of.
- * @param maxAngle The maximum angle that the arm is capable of.
+ * @param minAngle The minimum angle that the arm is capable of, with 0 being
+ * horizontal.
+ * @param maxAngle The maximum angle that the arm is capable of, with 0 being
+ * horizontal.
* @param simulateGravity Whether gravity should be simulated or not.
* @param startingAngle The initial position of the arm.
* @param measurementStdDevs The standard deviations of the measurements.
@@ -51,8 +53,10 @@ class SingleJointedArmSim : public LinearSystemSim<2, 1, 2> {
* @param moi The moment of inertia of the arm. This can be calculated from
* CAD software.
* @param armLength The length of the arm.
- * @param minAngle The minimum angle that the arm is capable of.
- * @param maxAngle The maximum angle that the arm is capable of.
+ * @param minAngle The minimum angle that the arm is capable of, with 0 being
+ * horizontal.
+ * @param maxAngle The maximum angle that the arm is capable of, with 0 being
+ * horizontal.
* @param simulateGravity Whether gravity should be simulated or not.
* @param startingAngle The initial position of the arm.
* @param measurementStdDevs The standard deviation of the measurement noise.
diff --git a/wpilibj/src/main/java/org/wpilib/simulation/SingleJointedArmSim.java b/wpilibj/src/main/java/org/wpilib/simulation/SingleJointedArmSim.java
index dfaf06725a..2152ffbc5b 100644
--- a/wpilibj/src/main/java/org/wpilib/simulation/SingleJointedArmSim.java
+++ b/wpilibj/src/main/java/org/wpilib/simulation/SingleJointedArmSim.java
@@ -43,10 +43,13 @@ public class SingleJointedArmSim extends LinearSystemSim {
* @param gearbox The type of and number of motors in the arm gearbox.
* @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
* @param armLength The length of the arm in meters.
- * @param minAngleRads The minimum angle that the arm is capable of.
- * @param maxAngleRads The maximum angle that the arm is capable of.
+ * @param minAngleRads The minimum angle that the arm is capable of, with 0 radians being
+ * horizontal.
+ * @param maxAngleRads The maximum angle that the arm is capable of, with 0 radians being
+ * horizontal.
* @param simulateGravity Whether gravity should be simulated or not.
- * @param startingAngleRads The initial position of the Arm simulation in radians.
+ * @param startingAngleRads The initial position of the Arm simulation in radians, with 0 radians
+ * being horizontal.
* @param measurementStdDevs The standard deviations of the measurements. Can be omitted if no
* noise is desired. If present must have 1 element for position.
*/
@@ -79,10 +82,13 @@ public class SingleJointedArmSim extends LinearSystemSim {
* @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
* @param j The moment of inertia of the arm in kg-m²; can be calculated from CAD software.
* @param armLength The length of the arm in meters.
- * @param minAngleRads The minimum angle that the arm is capable of.
- * @param maxAngleRads The maximum angle that the arm is capable of.
+ * @param minAngleRads The minimum angle that the arm is capable of, with 0 radians being
+ * horizontal.
+ * @param maxAngleRads The maximum angle that the arm is capable of, with 0 radians being
+ * horizontal.
* @param simulateGravity Whether gravity should be simulated or not.
- * @param startingAngleRads The initial position of the Arm simulation in radians.
+ * @param startingAngleRads The initial position of the Arm simulation in radians, with 0 radians
+ * being horizontal.
* @param measurementStdDevs The standard deviations of the measurements. Can be omitted if no
* noise is desired. If present must have 1 element for position.
*/
diff --git a/wpilibj/src/main/java/org/wpilib/util/Preferences.java b/wpilibj/src/main/java/org/wpilib/util/Preferences.java
index 62605a54b8..a3761f0131 100644
--- a/wpilibj/src/main/java/org/wpilib/util/Preferences.java
+++ b/wpilibj/src/main/java/org/wpilib/util/Preferences.java
@@ -82,6 +82,8 @@ public final class Preferences {
if (m_listener != null) {
m_listener.close();
}
+
+ Topic typePublisherTopic = m_typePublisher.getTopic();
m_listener =
NetworkTableListener.createListener(
m_tableSubscriber,
@@ -89,8 +91,8 @@ public final class Preferences {
event -> {
if (event.topicInfo != null) {
Topic topic = event.topicInfo.getTopic();
- if (!topic.equals(m_typePublisher.getTopic())) {
- event.topicInfo.getTopic().setPersistent(true);
+ if (!topic.equals(typePublisherTopic)) {
+ topic.setPersistent(true);
}
}
});
diff --git a/wpimath/src/main/java/org/wpilib/math/filter/SlewRateLimiter.java b/wpimath/src/main/java/org/wpilib/math/filter/SlewRateLimiter.java
index 58a78b76c9..8c741d1c1c 100644
--- a/wpimath/src/main/java/org/wpilib/math/filter/SlewRateLimiter.java
+++ b/wpimath/src/main/java/org/wpilib/math/filter/SlewRateLimiter.java
@@ -13,8 +13,8 @@ import org.wpilib.math.util.MathSharedStore;
* org.wpilib.math.trajectory.TrapezoidProfile} instead.
*/
public class SlewRateLimiter {
- private final double m_positiveRateLimit;
- private final double m_negativeRateLimit;
+ private double m_positiveRateLimit;
+ private double m_negativeRateLimit;
private double m_prevVal;
private double m_prevTime;
@@ -81,4 +81,29 @@ public class SlewRateLimiter {
m_prevVal = value;
m_prevTime = MathSharedStore.getTimestamp();
}
+
+ /**
+ * Sets the rate-of-change limit to the given positive and negative rate limits.
+ *
+ * @param positiveRateLimit The rate-of-change limit in the positive direction, in units per
+ * second. This is expected to be positive.
+ * @param negativeRateLimit The rate-of-change limit in the negative direction, in units per
+ * second. This is expected to be negative.
+ */
+ public void setLimit(double positiveRateLimit, double negativeRateLimit) {
+ m_positiveRateLimit = positiveRateLimit;
+ m_negativeRateLimit = negativeRateLimit;
+ }
+
+ /**
+ * Sets the rate-of-change limit to the given positive rate limit and negative rate limit of
+ * -rateLimit.
+ *
+ * @param rateLimit The rate-of-change limit in both directions, in units per second. This is
+ * expected to be positive.
+ */
+ public void setLimit(double rateLimit) {
+ m_positiveRateLimit = rateLimit;
+ m_negativeRateLimit = -rateLimit;
+ }
}
diff --git a/wpimath/src/main/native/include/wpi/math/filter/SlewRateLimiter.hpp b/wpimath/src/main/native/include/wpi/math/filter/SlewRateLimiter.hpp
index 86ac913f4e..3854f6910a 100644
--- a/wpimath/src/main/native/include/wpi/math/filter/SlewRateLimiter.hpp
+++ b/wpimath/src/main/native/include/wpi/math/filter/SlewRateLimiter.hpp
@@ -92,6 +92,32 @@ class SlewRateLimiter {
m_prevTime = wpi::math::MathSharedStore::GetTimestamp();
}
+ /**
+ * Sets the rate-of-change limit to the given positive and negative rate
+ * limits.
+ *
+ * @param positiveRateLimit The rate-of-change limit in the positive
+ * direction, in units per second. This is expected to be positive.
+ * @param negativeRateLimit The rate-of-change limit in the negative
+ * direction, in units per second. This is expected to be negative.
+ */
+ void SetLimit(Rate_t positiveRateLimit, Rate_t negativeRateLimit) {
+ m_positiveRateLimit = positiveRateLimit;
+ m_negativeRateLimit = negativeRateLimit;
+ }
+
+ /**
+ * Sets the rate-of-change limit to the given positive rate limit and negative
+ * rate limit of -rateLimit.
+ *
+ * @param rateLimit The rate-of-change limit in both directions, in units per
+ * second. This is expected to be positive.
+ */
+ void SetLimit(Rate_t rateLimit) {
+ m_positiveRateLimit = rateLimit;
+ m_negativeRateLimit = -rateLimit;
+ }
+
private:
Rate_t m_positiveRateLimit;
Rate_t m_negativeRateLimit;