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;