mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
Merge branch 'main' into 2027
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
29
docs/opencv/element-list
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
179
ntcore/src/test/native/cpp/NetworkServerTest.cpp
Normal file
179
ntcore/src/test/native/cpp/NetworkServerTest.cpp
Normal 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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user