Merge branch 'main' into 2027

This commit is contained in:
Peter Johnson
2026-02-27 14:24:41 -08:00
13 changed files with 317 additions and 19 deletions

View File

@@ -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.
*
* <p>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

View File

@@ -14,7 +14,7 @@ WaitUntilCommand::WaitUntilCommand(std::function<bool()> 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();

View File

@@ -35,7 +35,12 @@ class WaitUntilCommand : public CommandHelper<Command, WaitUntilCommand> {
* 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);

View File

@@ -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

29
docs/opencv/element-list Normal file
View File

@@ -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

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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 <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <thread>
#include <gtest/gtest.h>
#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);
}

View File

@@ -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.

View File

@@ -43,10 +43,13 @@ public class SingleJointedArmSim extends LinearSystemSim<N2, N1, N2> {
* @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<N2, N1, N2> {
* @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.
*/

View File

@@ -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);
}
}
});

View File

@@ -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;
}
}

View File

@@ -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;