From 9cd933fa1494a4e486102b17040d9cf9201b75cd Mon Sep 17 00:00:00 2001 From: Jordan Powers Date: Fri, 20 Feb 2026 15:29:53 -0800 Subject: [PATCH 1/7] [wpiunits] Fix incorrect magnitudes in some MutableMeasure mutations (#8620) This PR fixes the magnitude units in `MutableMeasure#mut_acc` and `MutableMeasure#mut_plus`. Previously, both `mut_acc` and `mut_plus` were setting the base magnitude using the unit-ed magnitude value. While this would work fine for base units where `magnitude == baseUnitMagnitude`, this was creating issues with derived units. This PR also adds missing tests for the `MutableMeasure` class. --- .../edu/wpi/first/units/MutableMeasure.java | 4 +- .../wpi/first/units/MutableMeasureTest.java | 168 ++++++++++++++++++ 2 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 wpiunits/src/test/java/edu/wpi/first/units/MutableMeasureTest.java diff --git a/wpiunits/src/main/java/edu/wpi/first/units/MutableMeasure.java b/wpiunits/src/main/java/edu/wpi/first/units/MutableMeasure.java index 672dbe61f0..d2944d8589 100644 --- a/wpiunits/src/main/java/edu/wpi/first/units/MutableMeasure.java +++ b/wpiunits/src/main/java/edu/wpi/first/units/MutableMeasure.java @@ -73,7 +73,7 @@ public interface MutableMeasure< * @return the measure */ default MutSelf mut_acc(double raw) { - return mut_setBaseUnitMagnitude(magnitude() + raw); + return mut_setMagnitude(magnitude() + raw); } /** @@ -107,7 +107,7 @@ public interface MutableMeasure< * @return this measure */ default MutSelf mut_plus(double magnitude, U otherUnit) { - return mut_setBaseUnitMagnitude(magnitude() + otherUnit.toBaseUnits(magnitude)); + return mut_setBaseUnitMagnitude(baseUnitMagnitude() + otherUnit.toBaseUnits(magnitude)); } /** diff --git a/wpiunits/src/test/java/edu/wpi/first/units/MutableMeasureTest.java b/wpiunits/src/test/java/edu/wpi/first/units/MutableMeasureTest.java new file mode 100644 index 0000000000..4a40455985 --- /dev/null +++ b/wpiunits/src/test/java/edu/wpi/first/units/MutableMeasureTest.java @@ -0,0 +1,168 @@ +// 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. + +package edu.wpi.first.units; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.MutDistance; +import org.junit.jupiter.api.Test; + +class MutableMeasureTest { + @Test + void testBasics() { + DistanceUnit unit = Units.Feet; + double magnitude = 10; + MutDistance m = unit.mutable(magnitude); + assertEquals(unit, m.unit(), "Wrong units"); + assertEquals(magnitude, m.magnitude(), 0, "Wrong magnitude"); + } + + @Test + void testMultiply() { + MutDistance m = Units.Feet.mutable(1); + MutDistance m2 = m.mut_times(Units.Value.of(10)); + assertEquals(10, m.in(Units.Feet), 1e-12); + assertSame(m, m2); + } + + @Test + void testMultiplyScalar() { + MutDistance m = Units.Feet.mutable(1); + MutDistance m2 = m.mut_times(10); + assertEquals(10, m.in(Units.Feet), 1e-12); + assertSame(m, m2); + } + + @Test + void testDivide() { + MutDistance m = Units.Meters.mutable(1); + MutDistance m2 = m.mut_divide(Units.Value.of(10)); + assertEquals(0.1, m.magnitude(), 0); + assertSame(m, m2); + } + + @Test + void testDivideScalar() { + MutDistance m = Units.Meters.mutable(1); + MutDistance m2 = m.mut_divide(10); + assertEquals(0.1, m.magnitude(), 0); + assertSame(m, m2); + } + + @Test + void testAdd() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance sum1 = m1.mut_plus(Units.Inches.of(2)); + assertTrue(sum1.isEquivalent(Units.Feet.of(1 + 2 / 12d))); + assertSame(m1, sum1); + + Distance sum2 = m2.mut_plus(Units.Feet.of(1)); + assertTrue(sum2.isEquivalent(Units.Inches.of(14))); + assertSame(m2, sum2); + } + + @Test + void testAddScalar() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance sum1 = m1.mut_plus(2, Units.Inches); + assertTrue(sum1.isEquivalent(Units.Feet.of(1 + 2 / 12d))); + assertSame(m1, sum1); + + Distance sum2 = m2.mut_plus(1, Units.Feet); + assertTrue(sum2.isEquivalent(Units.Inches.of(14))); + assertSame(m2, sum2); + } + + @Test + void testAcc() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance acc1 = m1.mut_acc(Units.Inches.of(2)); + assertTrue(acc1.isEquivalent(Units.Feet.of(1 + 2 / 12d))); + assertSame(m1, acc1); + + Distance acc2 = m2.mut_acc(Units.Feet.of(1)); + assertTrue(acc2.isEquivalent(Units.Inches.of(14))); + assertSame(m2, acc2); + } + + @Test + void testAccScalar() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance acc1 = m1.mut_acc(2 / 12d); + assertTrue(acc1.isEquivalent(Units.Feet.of(1 + 2 / 12d))); + assertSame(m1, acc1); + + Distance acc2 = m2.mut_acc(12); + assertTrue(acc2.isEquivalent(Units.Inches.of(14))); + assertSame(m2, acc2); + } + + @Test + void testSubtract() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance sub1 = m1.mut_minus(Units.Inches.of(2)); + assertTrue(sub1.isEquivalent(Units.Feet.of(1 - 2 / 12d))); + assertSame(m1, sub1); + + Distance sub2 = m2.mut_minus(Units.Feet.of(1)); + assertTrue(sub2.isEquivalent(Units.Inches.of(-10))); + assertSame(m2, sub2); + } + + @Test + void testSubtractScalar() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance sub1 = m1.mut_minus(2, Units.Inches); + assertTrue(sub1.isEquivalent(Units.Feet.of(1 - 2 / 12d))); + assertSame(m1, sub1); + + Distance sub2 = m2.mut_minus(1, Units.Feet); + assertTrue(sub2.isEquivalent(Units.Inches.of(-10))); + assertSame(m2, sub2); + } + + @Test + void testReplace() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance replace1 = m1.mut_replace(Units.Inches.of(2)); + assertTrue(replace1.isEquivalent(Units.Inches.of(2))); + assertSame(m1, replace1); + + Distance replace2 = m2.mut_replace(Units.Feet.of(1)); + assertTrue(replace2.isEquivalent(Units.Feet.of(1))); + assertSame(m2, replace2); + } + + @Test + void testReplaceScalar() { + MutDistance m1 = Units.Feet.mutable(1); + MutDistance m2 = Units.Inches.mutable(2); + + Distance replace1 = m1.mut_replace(2, Units.Inches); + assertTrue(replace1.isEquivalent(Units.Inches.of(2))); + assertSame(m1, replace1); + + Distance replace2 = m2.mut_replace(1, Units.Feet); + assertTrue(replace2.isEquivalent(Units.Feet.of(1))); + assertSame(m2, replace2); + } +} From d9eba4bb22797a37d0d9ef9c1807cd073f43b7b3 Mon Sep 17 00:00:00 2001 From: DeltaDizzy Date: Sat, 21 Feb 2026 17:36:36 -0500 Subject: [PATCH 2/7] [wpilib] Document zero angle in SingleJointedArmSim (NFC) (#7756) Fixes #7752 --------- Co-authored-by: sciencewhiz Co-authored-by: Tyler Veness --- .../frc/simulation/SingleJointedArmSim.h | 18 ++++++++++++------ .../simulation/SingleJointedArmSim.java | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/wpilibc/src/main/native/include/frc/simulation/SingleJointedArmSim.h b/wpilibc/src/main/native/include/frc/simulation/SingleJointedArmSim.h index 36924f8d0f..59d041b1ec 100644 --- a/wpilibc/src/main/native/include/frc/simulation/SingleJointedArmSim.h +++ b/wpilibc/src/main/native/include/frc/simulation/SingleJointedArmSim.h @@ -30,10 +30,13 @@ 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 startingAngle The initial position of the arm, with 0 being + * horizontal. * @param measurementStdDevs The standard deviations of the measurements. */ SingleJointedArmSim(const LinearSystem<2, 1, 2>& system, @@ -52,10 +55,13 @@ 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 startingAngle The initial position of the arm, with 0 being + * horizontal. * @param measurementStdDevs The standard deviation of the measurement noise. */ SingleJointedArmSim(const DCMotor& gearbox, double gearing, diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java index 7d7ae78284..bcb66ea45a 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java @@ -44,10 +44,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 armLengthMeters The length of the arm. - * @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. */ @@ -80,10 +83,13 @@ public class SingleJointedArmSim extends LinearSystemSim { * @param gearing The gearing of the arm (numbers greater than 1 represent reductions). * @param jKgMetersSquared The moment of inertia of the arm; can be calculated from CAD software. * @param armLengthMeters The length of the arm. - * @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. */ From 5ae8ee06dd99a9521261c98d0997e40dc023f5fa Mon Sep 17 00:00:00 2001 From: sciencewhiz Date: Mon, 23 Feb 2026 16:32:28 -0800 Subject: [PATCH 3/7] [build] use local opencv docs element-list (#8633) Fixes opencv cloudflare blocking gradle javadoc builds --- docs/build.gradle | 4 +++- docs/opencv/element-list | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 docs/opencv/element-list diff --git a/docs/build.gradle b/docs/build.gradle index 5a3784a883..cb2bee9279 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -152,7 +152,9 @@ configurations { task generateJavaDocs(type: Javadoc) { classpath += project(":wpilibj").sourceSets.main.compileClasspath options.links("https://docs.oracle.com/en/java/javase/17/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 From ae43b8b6dd0fe68212dea1305d41b3331bc6f31e Mon Sep 17 00:00:00 2001 From: sciencewhiz Date: Mon, 23 Feb 2026 17:11:59 -0800 Subject: [PATCH 4/7] [cmd] Fix WaitUntilCommand for match time counting down (#8632) Fixes #8631 Documents that it will return immediately if FMS isn't attached or DS isn't in practice mode. Related to change in DS match time behavior that was documented in #8606 --- .../edu/wpi/first/wpilibj2/command/WaitUntilCommand.java | 7 ++++++- .../src/main/native/cpp/frc2/command/WaitUntilCommand.cpp | 2 +- .../main/native/include/frc2/command/WaitUntilCommand.h | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/WaitUntilCommand.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/WaitUntilCommand.java index b96bc26a3a..78c4819977 100644 --- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/WaitUntilCommand.java +++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/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/wpilibNewCommands/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp b/wpilibNewCommands/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp index 9ea6fbfae9..5a24801927 100644 --- a/wpilibNewCommands/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp +++ b/wpilibNewCommands/src/main/native/cpp/frc2/command/WaitUntilCommand.cpp @@ -14,7 +14,7 @@ WaitUntilCommand::WaitUntilCommand(std::function condition) : m_condition{std::move(condition)} {} WaitUntilCommand::WaitUntilCommand(units::second_t time) - : m_condition{[=] { return frc::Timer::GetMatchTime() - time > 0_s; }} {} + : m_condition{[=] { return frc::Timer::GetMatchTime() < time; }} {} bool WaitUntilCommand::IsFinished() { return m_condition(); diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/WaitUntilCommand.h b/wpilibNewCommands/src/main/native/include/frc2/command/WaitUntilCommand.h index 377a1ba9a6..ea23885860 100644 --- a/wpilibNewCommands/src/main/native/include/frc2/command/WaitUntilCommand.h +++ b/wpilibNewCommands/src/main/native/include/frc2/command/WaitUntilCommand.h @@ -36,7 +36,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(units::second_t time); From e311722637edfa53d3e7fb277024e23aa55b4993 Mon Sep 17 00:00:00 2001 From: Stephen Just Date: Fri, 27 Feb 2026 12:39:05 -0800 Subject: [PATCH 5/7] [ntcore] Handle interrupted save in NetworkServer (#8630) In NetworkServer::SavePersistent, if the save is interrupted (by robot power loss, etc), the networktables.json file may be left in an unhandled state where the file consumed by NetworkServer::LoadPersistent is not found, but the backup file exists. In this case, we should attempt to recover the backup file to avoid losing all persistent data. --- ntcore/src/main/native/cpp/NetworkServer.cpp | 15 ++ .../src/test/native/cpp/NetworkServerTest.cpp | 179 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 ntcore/src/test/native/cpp/NetworkServerTest.cpp diff --git a/ntcore/src/main/native/cpp/NetworkServer.cpp b/ntcore/src/main/native/cpp/NetworkServer.cpp index 8fb419a12b..cb26fae387 100644 --- a/ntcore/src/main/native/cpp/NetworkServer.cpp +++ b/ntcore/src/main/native/cpp/NetworkServer.cpp @@ -362,6 +362,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::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..635c234aa3 --- /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 "networktables/IntegerTopic.h" +#include "networktables/NetworkTableInstance.h" + +// 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( + 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 = nt::NetworkTableInstance::Create(); + 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(); + 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 = nt::NetworkTableInstance::Create(); + 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(); + 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 = nt::NetworkTableInstance::Create(); + 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(); + 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 = 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(); + nt::NetworkTableInstance::Destroy(inst); +} From 613bd8854816bbdea93128c6f41e6ceef853ce09 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Fri, 27 Feb 2026 12:42:40 -0800 Subject: [PATCH 6/7] [wpilib] Make Preferences Listener not depend on mutable fields (#8607) The Listener installed by Preferences was referencing m_typePublisher which could be modified by a future call to setNetworkTableInstance(). Instead, reference a local. Also made Topic.m_handle final, to guarantee that Topic.equals() is thread-safe, and still work after the publisher has been closed. --- ntcore/src/main/java/edu/wpi/first/networktables/Topic.java | 4 ++-- .../src/main/java/edu/wpi/first/wpilibj/Preferences.java | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java index ae548c52ae..05ed5752ff 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java +++ b/ntcore/src/main/java/edu/wpi/first/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/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java index 28dde09e2a..a33ce23c38 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java @@ -83,6 +83,8 @@ public final class Preferences { if (m_listener != null) { m_listener.close(); } + + Topic typePublisherTopic = m_typePublisher.getTopic(); m_listener = NetworkTableListener.createListener( m_tableSubscriber, @@ -90,8 +92,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); } } }); From 7ca35e5678cf32caec6a1a866ca51d0136c4c398 Mon Sep 17 00:00:00 2001 From: amsam0 <44983869+amsam0@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:53:18 -0800 Subject: [PATCH 7/7] [wpimath] Add limit setters to SlewRateLimiter (#8581) This is just #7793 with requested changes applied. --- .../first/math/filter/SlewRateLimiter.java | 29 +++++++++++++++++-- .../include/frc/filter/SlewRateLimiter.h | 26 +++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/wpimath/src/main/java/edu/wpi/first/math/filter/SlewRateLimiter.java b/wpimath/src/main/java/edu/wpi/first/math/filter/SlewRateLimiter.java index d9eca58419..010f3bdd1e 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/filter/SlewRateLimiter.java +++ b/wpimath/src/main/java/edu/wpi/first/math/filter/SlewRateLimiter.java @@ -14,8 +14,8 @@ import edu.wpi.first.math.MathUtil; * edu.wpi.first.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; @@ -82,4 +82,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/frc/filter/SlewRateLimiter.h b/wpimath/src/main/native/include/frc/filter/SlewRateLimiter.h index f04aac4855..aed961526e 100644 --- a/wpimath/src/main/native/include/frc/filter/SlewRateLimiter.h +++ b/wpimath/src/main/native/include/frc/filter/SlewRateLimiter.h @@ -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;