Compare commits

...

16 Commits

Author SHA1 Message Date
Levi
859fb6c408 [hal] Fix USB serial port assignment in SerialHelper (#8689) 2026-06-07 18:02:13 -07:00
amsam0
7ca35e5678 [wpimath] Add limit setters to SlewRateLimiter (#8581)
This is just #7793 with requested changes applied.
2026-02-27 12:53:18 -08:00
Kevin Cooney
613bd88548 [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.
2026-02-27 12:42:40 -08:00
Stephen Just
e311722637 [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.
2026-02-27 12:39:05 -08:00
sciencewhiz
ae43b8b6dd [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
2026-02-23 17:11:59 -08:00
sciencewhiz
5ae8ee06dd [build] use local opencv docs element-list (#8633)
Fixes opencv cloudflare blocking gradle javadoc builds
2026-02-23 16:32:28 -08:00
DeltaDizzy
d9eba4bb22 [wpilib] Document zero angle in SingleJointedArmSim (NFC) (#7756)
Fixes #7752

---------

Co-authored-by: sciencewhiz <sciencewhiz@users.noreply.github.com>
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2026-02-21 14:36:36 -08:00
Jordan Powers
9cd933fa14 [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.
2026-02-20 15:29:53 -08:00
sciencewhiz
77dfad97c6 [ci] Use wpilib docker-run-action fork (#8615) 2026-02-14 11:32:57 -08:00
sciencewhiz
7ac0612397 [hal,wpilib] Update MatchTime docs for teleop and auto mode (NFC) (#8606)
Update Timer and JNI MatchTime functions to latest docs
2026-02-09 17:14:42 -08:00
Tyler Veness
2c5529d714 [wpimath] Speed up pose estimator correction computation (#8574)
In C++, we use a diagonal matrix to avoid an expensive matrix
multiplication. EJML doesn't have a diagonal matrix type, so in Java, we
use a double array and implement the multiplication manually.
2026-02-06 21:47:49 -08:00
Gavin P
7cd3790c7c [wpiunits] Make RPM an alias of RotationsPerMinute (#8595)
Currently the only name for this unit is `RPM`. This caused a bit of
confusion for a couple of my team members when we failed to find an RPM
unit, assuming it would be named `RotationsPerMinute` as is the standard
for almost all other units, such as `RotationsPerSecond`.

No corresponding changes have been made to wpilibc as it seems to
already work this way, with `rpm` being the abbreviation for
`revolutions_per_minute`.
2026-02-06 21:47:08 -08:00
NotTacos
664484306c [glass] Change match times for rebuilt (#8575)
The timer has changed for rebuilt's auto and teleop.
2026-02-06 21:39:12 -08:00
jpokornyiii
0a37317467 [examples] Adding XRP java and cpp examples for Timed Robot (#8599)
Adding an example (one C++ and one Java) for using TimedRobot with the
XRP.
2026-02-06 21:36:35 -08:00
FirstFox3
6b225bb1f1 [wpilib] Fix typo in TimedRobot.getLoopStartTime() docs (#8590)
Fixed a typo in the description of the getLoopStartTime function in both
C++ and Java TimedRobot class.

---------

Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2026-02-05 18:57:49 -07:00
Vasista Vovveti
3d92547d62 [cscore] Fix format specifier on Mac (#8602) 2026-02-05 18:56:52 -07:00
40 changed files with 859 additions and 148 deletions

View File

@@ -52,7 +52,7 @@ jobs:
run: echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '2027')
- name: Build with Gradle
uses: addnab/docker-run-action@3e77f186b7a929ef010f183a9e24c0f9955ea609
uses: wpilibsuite/docker-run-action@v4
with:
image: ${{ matrix.container }}
options: -v ${{ github.workspace }}:/work -w /work -e ARTIFACTORY_PUBLISH_USERNAME -e ARTIFACTORY_PUBLISH_PASSWORD -e GITHUB_REF -e CI

View File

@@ -53,7 +53,7 @@ jobs:
with:
fetch-depth: 0
- name: Build with Gradle
uses: addnab/docker-run-action@3e77f186b7a929ef010f183a9e24c0f9955ea609
uses: wpilibsuite/docker-run-action@v4
with:
image: ${{ matrix.container }}
options: -v ${{ github.workspace }}:/work -w /work -e GITHUB_REF -e CI

View File

@@ -31,11 +31,11 @@ jobs:
fetch-depth: 0
- uses: gradle/actions/wrapper-validation@v4
- name: Build WPILib with Gradle
uses: addnab/docker-run-action@v3
uses: wpilibsuite/docker-run-action@v4
with:
image: wpilib/roborio-cross-ubuntu:2025-22.04
options: -v ${{ github.workspace }}:/work -w /work -e GITHUB_REF -e CI -e DISPLAY
run: df . && rm -f semicolon_delimited_script && ./gradlew :wpilibc:publish :wpilibj:publish :wpilibNewCommands:publish :hal:publish :cameraserver:publish :ntcore:publish :cscore:publish :wpimath:publish :wpinet:publish :wpiutil:publish :apriltag:publish :wpiunits:publish :simulation:halsim_gui:publish :simulation:halsim_ds_socket:publish :simulation:halsim_ws_server:publish :simulation:halsim_ws_client:publish :simulation:halsim_xrp:publish :fieldImages:publish :romiVendordep:publish :xrpVendordep:publish :epilogue-processor:publish :epilogue-runtime:publish :thirdparty:googletest:publish -x test -x Javadoc -x doxygen --build-cache && cp -r /root/releases/maven/development /work
run: df . && ./gradlew :wpilibc:publish :wpilibj:publish :wpilibNewCommands:publish :hal:publish :cameraserver:publish :ntcore:publish :cscore:publish :wpimath:publish :wpinet:publish :wpiutil:publish :apriltag:publish :wpiunits:publish :simulation:halsim_gui:publish :simulation:halsim_ds_socket:publish :simulation:halsim_ws_server:publish :simulation:halsim_ws_client:publish :simulation:halsim_xrp:publish :fieldImages:publish :romiVendordep:publish :xrpVendordep:publish :epilogue-processor:publish :epilogue-runtime:publish :thirdparty:googletest:publish -x test -x Javadoc -x doxygen --build-cache && cp -r /root/releases/maven/development /work
- uses: actions/upload-artifact@v4
with:
name: MavenArtifacts

View File

@@ -484,7 +484,7 @@ const propertyInfo_t propertyInfo[] =
UVCERROR("USB control interface not found");
break;
default:
UVCERROR("ControlRequest failed (KR=sys:sub:code) = {:02Xh}:{:03Xh}:{:04Xh}",
UVCERROR("ControlRequest failed (KR=sys:sub:code) = {:02X}h:{:03X}h:{:04X}h",
sys, sub, code);
break;
}

View File

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

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

@@ -71,11 +71,11 @@ void glass::DisplayFMS(FMSModel* model, bool editableDsAttached) {
}
ImGui::SameLine();
if (ImGui::Button("Auto") && !enabled) {
model->SetMatchTime(15.0);
model->SetMatchTime(20.0);
}
ImGui::SameLine();
if (ImGui::Button("Teleop") && !enabled) {
model->SetMatchTime(135.0);
model->SetMatchTime(140.0);
}
}

View File

@@ -308,16 +308,19 @@ public class DriverStationJNI extends JNIWrapper {
public static native int getJoystickAxisType(byte joystickNum, byte axis);
/**
* Returns the approximate match time.
* Return the approximate match time. The FMS does not send an official match time to the robots,
* but does send an approximate match time. The value will count down the time remaining in the
* current period (auto or teleop). Warning: This is not an official time (so it cannot be used to
* dispute ref calls or guarantee that a function will trigger before the match ends).
*
* <p>The FMS does not send an official match time to the robots, but does send an approximate
* match time. The value will count down the time remaining in the current period (auto or
* teleop).
* <p>When connected to the real field, this number only changes in full integer increments, and
* always counts down.
*
* <p>Warning: This is not an official time (so it cannot be used to dispute ref calls or
* guarantee that a function will trigger before the match ends).
* <p>When the DS is in practice mode, this number is a floating point number, and counts down.
*
* <p>The Practice Match function of the DS approximates the behavior seen on the field.
* <p>When the DS is in teleop or autonomous mode, this number returns -1.0.
*
* <p>Simulation matches DS behavior without an FMS connected.
*
* @return time remaining in current match period (auto or teleop)
* @see "HAL_GetMatchTime"

View File

@@ -138,10 +138,10 @@ void SerialHelper::SortHubPathVector() {
m_sortedHubPath = m_unsortedHubPath;
std::sort(m_sortedHubPath.begin(), m_sortedHubPath.end(),
[](const wpi::SmallVectorImpl<char>& lhs,
const wpi::SmallVectorImpl<char>& rhs) -> int {
const wpi::SmallVectorImpl<char>& rhs) -> bool {
std::string_view lhsRef(lhs.begin(), lhs.size());
std::string_view rhsRef(rhs.begin(), rhs.size());
return lhsRef.compare(rhsRef);
return lhsRef < rhsRef;
});
}
@@ -162,6 +162,11 @@ void SerialHelper::CoiteratedSort(
}
void SerialHelper::QueryHubPaths(int32_t* status) {
m_unsortedHubPath.clear();
m_visaResource.clear();
m_osResource.clear();
m_sortedHubPath.clear();
// VISA resource matching string
const char* str = "?*";
// Items needed for VISA
@@ -275,12 +280,14 @@ void SerialHelper::QueryHubPaths(int32_t* status) {
continue;
}
std::string_view hubPathComponent = pathSplitVec[hubIndex - 2];
std::string_view osPath =
wpi::split(wpi::split(osName, "(").second, ")").first;
// Add our devices to our list
m_unsortedHubPath.emplace_back(
std::string_view{pathSplitVec[hubIndex - 2]});
m_unsortedHubPath.emplace_back(hubPathComponent);
m_visaResource.emplace_back(desc);
m_osResource.emplace_back(
wpi::split(wpi::split(osName, "(").second, ")").first);
m_osResource.emplace_back(osPath);
break;
}
}
@@ -303,14 +310,16 @@ int32_t SerialHelper::GetIndexForPort(HAL_SerialPort port, int32_t* status) {
// If port has not been assigned, find the one to assign
if (portString.empty()) {
// local copy to avoid modifying original
auto availableHubPaths = m_sortedHubPath;
for (size_t i = 0; i < 2; i++) {
// Remove all used ports
// Remove all used ports from the local copy
auto idx = std::find_if(
m_sortedHubPath.begin(), m_sortedHubPath.end(),
availableHubPaths.begin(), availableHubPaths.end(),
[&](const auto& s) { return wpi::equals(s, m_usbNames[i]); });
if (idx != m_sortedHubPath.end()) {
// found
m_sortedHubPath.erase(idx);
if (idx != availableHubPaths.end()) {
availableHubPaths.erase(idx);
}
if (m_usbNames[i] == "") {
indices.push_back(i);
@@ -330,12 +339,12 @@ int32_t SerialHelper::GetIndexForPort(HAL_SerialPort port, int32_t* status) {
return -1;
}
if (idx >= static_cast<int32_t>(m_sortedHubPath.size())) {
if (idx >= static_cast<int32_t>(availableHubPaths.size())) {
*status = HAL_SERIAL_PORT_NOT_FOUND;
return -1;
}
portString = m_sortedHubPath[idx].str();
portString = availableHubPaths[idx].str();
m_usbNames[port - 2] = portString;
}

View File

@@ -187,8 +187,7 @@ int32_t HAL_SetJoystickOutputs(int32_t joystickNum, int64_t outputs,
* <p>When the DS is in practice mode, this number is a floating point number,
* and counts down.
*
* <p>When the DS is in teleop or autonomous mode, this number is a floating
* point number, and counts up.
* <p>When the DS is in teleop or autonomous mode, this number returns -1.0.
*
* <p>Simulation matches DS behavior without an FMS connected.
*

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

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

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

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(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();

View File

@@ -36,7 +36,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(units::second_t time);

View File

@@ -339,8 +339,7 @@ class DriverStation final {
* <p>When the DS is in practice mode, this number is a floating point number,
* and counts down.
*
* <p>When the DS is in teleop or autonomous mode, this number is a floating
* point number, and counts up.
* <p>When the DS is in teleop or autonomous mode, this number returns -1.0.
*
* <p>Simulation matches DS behavior without an FMS connected.
*

View File

@@ -65,7 +65,7 @@ class TimedRobot : public IterativeRobotBase {
~TimedRobot() override;
/**
* Return the system clock time in micrseconds for the start of the current
* Return the system clock time in microseconds for the start of the current
* periodic loop. This is in the same time base as Timer.GetFPGATimestamp(),
* but is stable through a loop. It is updated at the beginning of every
* periodic callback (including the normal periodic loop).

View File

@@ -139,19 +139,23 @@ class Timer {
static units::second_t GetFPGATimestamp();
/**
* Return the approximate match time.
*
* The FMS does not send an official match time to the robots, but does send
* an approximate match time. The value will count down the time remaining in
* the current period (auto or teleop).
*
* Return the approximate match time. The FMS does not send an official match
* time to the robots, but does send an approximate match time. The value will
* count down the time remaining in the current period (auto or teleop).
* Warning: This is not an official time (so it cannot be used to dispute ref
* calls or guarantee that a function will trigger before the match ends).
*
* The Practice Match function of the DS approximates the behavior seen on the
* field.
* <p>When connected to the real field, this number only changes in full
* integer increments, and always counts down.
*
* @return Time remaining in current match period (auto or teleop)
* <p>When the DS is in practice mode, this number is a floating point number,
* and counts down.
*
* <p>When the DS is in teleop or autonomous mode, this number returns -1.0.
*
* <p>Simulation matches DS behavior without an FMS connected.
*
* @return Time remaining in current match period (auto or teleop) in seconds
*/
static units::second_t GetMatchTime();

View File

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

View File

@@ -0,0 +1,57 @@
// 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 "Robot.h"
Robot::Robot() {
wpi::SendableRegistry::AddChild(&m_drive, &m_leftMotor);
wpi::SendableRegistry::AddChild(&m_drive, &m_rightMotor);
// We need to invert one side of the drivetrain so that positive voltages
// result in both sides moving forward. Depending on how your robot's
// gearbox is constructed, you might have to invert the left side instead.
m_rightMotor.SetInverted(true);
}
/**
* This function is called every 20 ms, no matter the mode. Use
* this for items like diagnostics that you want ran during disabled,
* autonomous, teleoperated and test.
*
* <p> This runs after the mode specific periodic functions, but before
* LiveWindow and SmartDashboard integrated updating.
*/
void Robot::RobotPeriodic() {}
// This function is only once each time Autonomous is enabled
void Robot::AutonomousInit() {
m_timer.Restart();
}
// This function is called periodically during autonomous mode
void Robot::AutonomousPeriodic() {
// Drive for 2 seconds
if (m_timer.Get() < 2_s) {
// Drive forwards half speed, make sure to turn input squaring off
m_drive.ArcadeDrive(0.5, 0.0, false);
} else {
// Stop robot
m_drive.ArcadeDrive(0.0, 0.0, false);
}
}
// This function is only once each time telop is enabled
void Robot::TeleopInit() {}
// This function is called periodically during teleop mode
void Robot::TeleopPeriodic() {
m_drive.ArcadeDrive(-m_XboxController.GetLeftY(),
-m_XboxController.GetRightX());
}
#ifndef RUNNING_FRC_TESTS
int main() {
return frc::StartRobot<Robot>();
}
#endif

View File

@@ -0,0 +1,31 @@
// 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.
#pragma once
#include <frc/TimedRobot.h>
#include <frc/Timer.h>
#include <frc/XboxController.h>
#include <frc/drive/DifferentialDrive.h>
#include <frc/xrp/XRPMotor.h>
class Robot : public frc::TimedRobot {
public:
Robot();
void RobotPeriodic() override;
void AutonomousInit() override;
void AutonomousPeriodic() override;
void TeleopInit() override;
void TeleopPeriodic() override;
private:
frc::XRPMotor m_leftMotor{0};
frc::XRPMotor m_rightMotor{1};
frc::XboxController m_XboxController{0};
frc::Timer m_timer;
frc::DifferentialDrive m_drive{
[&](double output) { m_leftMotor.Set(output); },
[&](double output) { m_rightMotor.Set(output); }};
};

View File

@@ -609,6 +609,22 @@
"xrp"
]
},
{
"name": "XRP Timed",
"description": "An very basic timed robot program that can be used with the XRP reference robot design.",
"tags": [
"XRP",
"Differential Drive",
"XboxController"
],
"foldername": "Xrptimed",
"gradlebase": "cppxrp",
"mainclass": "Main",
"commandversion": 2,
"extravendordeps": [
"xrp"
]
},
{
"name": "StateSpaceFlywheel",
"description": "Control a flywheel using a state-space model (based on values from CAD), with a Kalman Filter and LQR.",

View File

@@ -1182,8 +1182,7 @@ public final class DriverStation {
*
* <p>When the DS is in practice mode, this number is a floating point number, and counts down.
*
* <p>When the DS is in teleop or autonomous mode, this number is a floating point number, and
* counts up.
* <p>When the DS is in teleop or autonomous mode, this number returns -1.0.
*
* <p>Simulation matches DS behavior without an FMS connected.
*

View File

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

View File

@@ -181,9 +181,9 @@ public class TimedRobot extends IterativeRobotBase {
}
/**
* Return the system clock time in micrseconds for the start of the current periodic loop. This is
* in the same time base as Timer.getFPGATimestamp(), but is stable through a loop. It is updated
* at the beginning of every periodic callback (including the normal periodic loop).
* Return the system clock time in microseconds for the start of the current periodic loop. This
* is in the same time base as Timer.getFPGATimestamp(), but is stable through a loop. It is
* updated at the beginning of every periodic callback (including the normal periodic loop).
*
* @return Robot running time in microseconds, as of the start of the current periodic function.
*/

View File

@@ -40,8 +40,16 @@ public class Timer {
* Return the approximate match time. The FMS does not send an official match time to the robots,
* but does send an approximate match time. The value will count down the time remaining in the
* current period (auto or teleop). Warning: This is not an official time (so it cannot be used to
* dispute ref calls or guarantee that a function will trigger before the match ends) The Practice
* Match function of the DS approximates the behavior seen on the field.
* dispute ref calls or guarantee that a function will trigger before the match ends).
*
* <p>When connected to the real field, this number only changes in full integer increments, and
* always counts down.
*
* <p>When the DS is in practice mode, this number is a floating point number, and counts down.
*
* <p>When the DS is in teleop or autonomous mode, this number returns -1.0.
*
* <p>Simulation matches DS behavior without an FMS connected.
*
* @return Time remaining in current match period (auto or teleop) in seconds
*/

View File

@@ -44,10 +44,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 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<N2, N1, N2> {
* @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.
*/

View File

@@ -812,6 +812,22 @@
"xrp"
]
},
{
"name": "XRP Timed",
"description": "An very basic timed robot program that can be used with the XRP reference robot design.",
"tags": [
"XRP",
"Differential Drive",
"XboxController"
],
"foldername": "xrptimed",
"gradlebase": "javaxrp",
"mainclass": "Main",
"commandversion": 2,
"extravendordeps": [
"xrp"
]
},
{
"name": "Digital Communication Sample",
"description": "Communicates with external devices (such as an Arduino) using the roboRIO's DIO.",

View File

@@ -0,0 +1,25 @@
// 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.wpilibj.examples.xrptimed;
import edu.wpi.first.wpilibj.RobotBase;
/**
* Do NOT add any static variables to this class, or any initialization at all. Unless you know what
* you are doing, do not modify this file except to change the parameter class to the startRobot
* call.
*/
public final class Main {
private Main() {}
/**
* Main initialization function. Do not perform any initialization here.
*
* <p>If you change your main robot class, change the parameter type.
*/
public static void main(String... args) {
RobotBase.startRobot(Robot::new);
}
}

View File

@@ -0,0 +1,73 @@
// 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.wpilibj.examples.xrptimed;
import edu.wpi.first.util.sendable.SendableRegistry;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj.Timer;
import edu.wpi.first.wpilibj.XboxController;
import edu.wpi.first.wpilibj.drive.DifferentialDrive;
import edu.wpi.first.wpilibj.xrp.XRPMotor;
/**
* The methods in this class are called automatically corresponding to each mode, as described in
* the TimedRobot documentation. If you change the name of this class or the package after creating
* this project, you must also update the manifest file in the resource directory.
*/
public class Robot extends TimedRobot {
private final XRPMotor m_leftDrive = new XRPMotor(0);
private final XRPMotor m_rightDrive = new XRPMotor(1);
private final DifferentialDrive m_robotDrive =
new DifferentialDrive(m_leftDrive::set, m_rightDrive::set);
private final XboxController m_controller = new XboxController(0);
private final Timer m_timer = new Timer();
/** Called once at the beginning of the robot program. */
public Robot() {
SendableRegistry.addChild(m_robotDrive, m_leftDrive);
SendableRegistry.addChild(m_robotDrive, m_rightDrive);
// We need to invert one side of the drivetrain so that positive voltages
// result in both sides moving forward. Depending on how your robot's
// gearbox is constructed, you might have to invert the left side instead.
m_rightDrive.setInverted(true);
}
/** This function is run once each time the robot enters autonomous mode. */
@Override
public void autonomousInit() {
m_timer.restart();
}
/** This function is called periodically during autonomous. */
@Override
public void autonomousPeriodic() {
// Drive for 2 seconds
if (m_timer.get() < 2.0) {
// Drive forwards half speed, make sure to turn input squaring off
m_robotDrive.arcadeDrive(0.5, 0.0, false);
} else {
m_robotDrive.stopMotor(); // stop robot
}
}
/** This function is called once each time the robot enters teleoperated mode. */
@Override
public void teleopInit() {}
/** This function is called periodically during teleoperated mode. */
@Override
public void teleopPeriodic() {
m_robotDrive.arcadeDrive(-m_controller.getLeftY(), -m_controller.getRightX());
}
/** This function is called once each time the robot enters test mode. */
@Override
public void testInit() {}
/** This function is called periodically during test mode. */
@Override
public void testPeriodic() {}
}

View File

@@ -8,8 +8,6 @@ import edu.wpi.first.math.MathSharedStore;
import edu.wpi.first.math.MathUsageId;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Transform2d;
@@ -40,8 +38,12 @@ import java.util.TreeMap;
*/
public class PoseEstimator<T> {
private final Odometry<T> m_odometry;
private final Matrix<N3, N1> m_q = new Matrix<>(Nat.N3(), Nat.N1());
private final Matrix<N3, N3> m_visionK = new Matrix<>(Nat.N3(), Nat.N3());
// Diagonal of process noise covariance matrix Q
private final double[] m_q = new double[] {0.0, 0.0, 0.0};
// Diagonal of Kalman gain matrix K
private final double[] m_vision_k = new double[] {0.0, 0.0, 0.0};
private static final double kBufferDuration = 1.5;
// Maps timestamps to odometry-only pose estimates
@@ -79,7 +81,7 @@ public class PoseEstimator<T> {
m_poseEstimate = m_odometry.getPoseMeters();
for (int i = 0; i < 3; ++i) {
m_q.set(i, 0, stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0));
m_q[i] = stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0);
}
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
MathSharedStore.getMathShared().reportUsage(MathUsageId.kEstimator_PoseEstimator, 1);
@@ -95,6 +97,7 @@ public class PoseEstimator<T> {
* theta]ᵀ, with units in meters and radians.
*/
public final void setVisionMeasurementStdDevs(Matrix<N3, N1> visionMeasurementStdDevs) {
// Diagonal of measurement noise covariance matrix R
var r = new double[3];
for (int i = 0; i < 3; ++i) {
r[i] = visionMeasurementStdDevs.get(i, 0) * visionMeasurementStdDevs.get(i, 0);
@@ -103,11 +106,10 @@ public class PoseEstimator<T> {
// Solve for closed form Kalman gain for continuous Kalman filter with A = 0
// and C = I. See wpimath/algorithms.md.
for (int row = 0; row < 3; ++row) {
if (m_q.get(row, 0) == 0.0) {
m_visionK.set(row, row, 0.0);
if (m_q[row] == 0.0) {
m_vision_k[row] = 0.0;
} else {
m_visionK.set(
row, row, m_q.get(row, 0) / (m_q.get(row, 0) + Math.sqrt(m_q.get(row, 0) * r[row])));
m_vision_k[row] = m_q[row] / (m_q[row] + Math.sqrt(m_q[row] * r[row]));
}
}
}
@@ -307,29 +309,23 @@ public class PoseEstimator<T> {
var transform = visionRobotPoseMeters.minus(visionSample.get());
// Step 5: We should not trust the transform entirely, so instead we scale this transform by a
// Kalman
// gain matrix representing how much we trust vision measurements compared to our current pose.
var k_times_transform =
m_visionK.times(
VecBuilder.fill(
transform.getX(), transform.getY(), transform.getRotation().getRadians()));
// Step 6: Convert back to Transform2d.
// Kalman gain matrix representing how much we trust vision measurements compared to our current
// pose. Then, we convert the result back to a Transform2d.
var scaledTransform =
new Transform2d(
k_times_transform.get(0, 0),
k_times_transform.get(1, 0),
Rotation2d.fromRadians(k_times_transform.get(2, 0)));
m_vision_k[0] * transform.getX(),
m_vision_k[1] * transform.getY(),
Rotation2d.fromRadians(m_vision_k[2] * transform.getRotation().getRadians()));
// Step 7: Calculate and record the vision update.
// Step 6: Calculate and record the vision update.
var visionUpdate =
new VisionUpdate(visionSample.get().plus(scaledTransform), odometrySample.get());
m_visionUpdates.put(timestampSeconds, visionUpdate);
// Step 8: Remove later vision measurements. (Matches previous behavior)
// Step 7: Remove later vision measurements. (Matches previous behavior)
m_visionUpdates.tailMap(timestampSeconds, false).entrySet().clear();
// Step 9: Update latest pose estimate. Since we cleared all updates after this vision update,
// Step 8: Update latest pose estimate. Since we cleared all updates after this vision update,
// it's guaranteed to be the latest vision update.
m_poseEstimate = visionUpdate.compensate(m_odometry.getPoseMeters());
}

View File

@@ -8,8 +8,6 @@ import edu.wpi.first.math.MathSharedStore;
import edu.wpi.first.math.MathUsageId;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation2d;
@@ -22,7 +20,6 @@ import edu.wpi.first.math.kinematics.Kinematics;
import edu.wpi.first.math.kinematics.Odometry3d;
import edu.wpi.first.math.numbers.N1;
import edu.wpi.first.math.numbers.N4;
import edu.wpi.first.math.numbers.N6;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.TreeMap;
@@ -48,8 +45,12 @@ import java.util.TreeMap;
*/
public class PoseEstimator3d<T> {
private final Odometry3d<T> m_odometry;
private final Matrix<N4, N1> m_q = new Matrix<>(Nat.N4(), Nat.N1());
private final Matrix<N6, N6> m_visionK = new Matrix<>(Nat.N6(), Nat.N6());
// Diagonal of process noise covariance matrix Q
private final double[] m_q = new double[] {0.0, 0.0, 0.0, 0.0};
// Diagonal of Kalman gain matrix K
private final double[] m_vision_k = new double[] {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
private static final double kBufferDuration = 1.5;
// Maps timestamps to odometry-only pose estimates
@@ -87,7 +88,7 @@ public class PoseEstimator3d<T> {
m_poseEstimate = m_odometry.getPoseMeters();
for (int i = 0; i < 4; ++i) {
m_q.set(i, 0, stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0));
m_q[i] = stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0);
}
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
MathSharedStore.getMathShared().reportUsage(MathUsageId.kEstimator_PoseEstimator3d, 1);
@@ -103,6 +104,7 @@ public class PoseEstimator3d<T> {
* theta]ᵀ, with units in meters and radians.
*/
public final void setVisionMeasurementStdDevs(Matrix<N4, N1> visionMeasurementStdDevs) {
// Diagonal of measurement covariance matrix R
var r = new double[4];
for (int i = 0; i < 4; ++i) {
r[i] = visionMeasurementStdDevs.get(i, 0) * visionMeasurementStdDevs.get(i, 0);
@@ -111,17 +113,16 @@ public class PoseEstimator3d<T> {
// Solve for closed form Kalman gain for continuous Kalman filter with A = 0
// and C = I. See wpimath/algorithms.md.
for (int row = 0; row < 4; ++row) {
if (m_q.get(row, 0) == 0.0) {
m_visionK.set(row, row, 0.0);
if (m_q[row] == 0.0) {
m_vision_k[row] = 0.0;
} else {
m_visionK.set(
row, row, m_q.get(row, 0) / (m_q.get(row, 0) + Math.sqrt(m_q.get(row, 0) * r[row])));
m_vision_k[row] = m_q[row] / (m_q[row] + Math.sqrt(m_q[row] * r[row]));
}
}
// Fill in the gains for the other components of the rotation vector
double angle_gain = m_visionK.get(3, 3);
m_visionK.set(4, 4, angle_gain);
m_visionK.set(5, 5, angle_gain);
double angle_gain = m_vision_k[3];
m_vision_k[4] = angle_gain;
m_vision_k[5] = angle_gain;
}
/**
@@ -320,38 +321,27 @@ public class PoseEstimator3d<T> {
var transform = visionRobotPoseMeters.minus(visionSample.get());
// Step 5: We should not trust the transform entirely, so instead we scale this transform by a
// Kalman
// gain matrix representing how much we trust vision measurements compared to our current pose.
var k_times_transform =
m_visionK.times(
VecBuilder.fill(
transform.getX(),
transform.getY(),
transform.getZ(),
transform.getRotation().getX(),
transform.getRotation().getY(),
transform.getRotation().getZ()));
// Step 6: Convert back to Transform3d.
// Kalman gain matrix representing how much we trust vision measurements compared to our current
// pose. Then we convert the result back to a Transform3d.
var scaledTransform =
new Transform3d(
k_times_transform.get(0, 0),
k_times_transform.get(1, 0),
k_times_transform.get(2, 0),
m_vision_k[0] * transform.getX(),
m_vision_k[1] * transform.getY(),
m_vision_k[2] * transform.getZ(),
new Rotation3d(
k_times_transform.get(3, 0),
k_times_transform.get(4, 0),
k_times_transform.get(5, 0)));
m_vision_k[3] * transform.getRotation().getX(),
m_vision_k[4] * transform.getRotation().getY(),
m_vision_k[5] * transform.getRotation().getZ()));
// Step 7: Calculate and record the vision update.
// Step 6: Calculate and record the vision update.
var visionUpdate =
new VisionUpdate(visionSample.get().plus(scaledTransform), odometrySample.get());
m_visionUpdates.put(timestampSeconds, visionUpdate);
// Step 8: Remove later vision measurements. (Matches previous behavior)
// Step 7: Remove later vision measurements. (Matches previous behavior)
m_visionUpdates.tailMap(timestampSeconds, false).entrySet().clear();
// Step 9: Update latest pose estimate. Since we cleared all updates after this vision update,
// Step 8: Update latest pose estimate. Since we cleared all updates after this vision update,
// it's guaranteed to be the latest vision update.
m_poseEstimate = visionUpdate.compensate(m_odometry.getPoseMeters());
}

View File

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

View File

@@ -86,6 +86,7 @@ class WPILIB_DLLEXPORT PoseEstimator {
*/
void SetVisionMeasurementStdDevs(
const wpi::array<double, 3>& visionMeasurementStdDevs) {
// Diagonal of measurement covariance matrix R
wpi::array<double, 3> r{wpi::empty_array};
for (size_t i = 0; i < 3; ++i) {
r[i] = visionMeasurementStdDevs[i] * visionMeasurementStdDevs[i];
@@ -95,9 +96,9 @@ class WPILIB_DLLEXPORT PoseEstimator {
// and C = I. See wpimath/algorithms.md.
for (size_t row = 0; row < 3; ++row) {
if (m_q[row] == 0.0) {
m_visionK(row, row) = 0.0;
m_vision_K.diagonal()[row] = 0.0;
} else {
m_visionK(row, row) =
m_vision_K.diagonal()[row] =
m_q[row] / (m_q[row] + std::sqrt(m_q[row] * r[row]));
}
}
@@ -318,9 +319,9 @@ class WPILIB_DLLEXPORT PoseEstimator {
// this transform by a Kalman gain matrix representing how much we trust
// vision measurements compared to our current pose.
Eigen::Vector3d k_times_transform =
m_visionK * Eigen::Vector3d{transform.X().value(),
transform.Y().value(),
transform.Rotation().Radians().value()};
m_vision_K * Eigen::Vector3d{transform.X().value(),
transform.Y().value(),
transform.Rotation().Radians().value()};
// Step 6: Convert back to Transform2d.
Transform2d scaledTransform{
@@ -475,8 +476,13 @@ class WPILIB_DLLEXPORT PoseEstimator {
static constexpr units::second_t kBufferDuration = 1.5_s;
Odometry<WheelSpeeds, WheelPositions>& m_odometry;
// Diagonal of process noise covariance matrix Q
wpi::array<double, 3> m_q{wpi::empty_array};
Eigen::Matrix3d m_visionK = Eigen::Matrix3d::Zero();
// Kalman gain matrix K
Eigen::DiagonalMatrix<double, 3> m_vision_K =
Eigen::DiagonalMatrix<double, 3>::Zero();
// Maps timestamps to odometry-only pose estimates
TimeInterpolatableBuffer<Pose2d> m_odometryPoseBuffer{kBufferDuration};

View File

@@ -92,6 +92,7 @@ class WPILIB_DLLEXPORT PoseEstimator3d {
*/
void SetVisionMeasurementStdDevs(
const wpi::array<double, 4>& visionMeasurementStdDevs) {
// Diagonal of measurement noise covariance matrix R
wpi::array<double, 4> r{wpi::empty_array};
for (size_t i = 0; i < 4; ++i) {
r[i] = visionMeasurementStdDevs[i] * visionMeasurementStdDevs[i];
@@ -101,15 +102,15 @@ class WPILIB_DLLEXPORT PoseEstimator3d {
// and C = I. See wpimath/algorithms.md.
for (size_t row = 0; row < 4; ++row) {
if (m_q[row] == 0.0) {
m_visionK(row, row) = 0.0;
m_vision_K.diagonal()[row] = 0.0;
} else {
m_visionK(row, row) =
m_vision_K.diagonal()[row] =
m_q[row] / (m_q[row] + std::sqrt(m_q[row] * r[row]));
}
}
double angle_gain = m_visionK(3, 3);
m_visionK(4, 4) = angle_gain;
m_visionK(5, 5) = angle_gain;
double angle_gain = m_vision_K.diagonal()[3];
m_vision_K.diagonal()[4] = angle_gain;
m_vision_K.diagonal()[5] = angle_gain;
}
/**
@@ -327,12 +328,12 @@ class WPILIB_DLLEXPORT PoseEstimator3d {
// this transform by a Kalman gain matrix representing how much we trust
// vision measurements compared to our current pose.
frc::Vectord<6> k_times_transform =
m_visionK * frc::Vectord<6>{transform.X().value(),
transform.Y().value(),
transform.Z().value(),
transform.Rotation().X().value(),
transform.Rotation().Y().value(),
transform.Rotation().Z().value()};
m_vision_K * frc::Vectord<6>{transform.X().value(),
transform.Y().value(),
transform.Z().value(),
transform.Rotation().X().value(),
transform.Rotation().Y().value(),
transform.Rotation().Z().value()};
// Step 6: Convert back to Transform3d.
Transform3d scaledTransform{
@@ -490,8 +491,13 @@ class WPILIB_DLLEXPORT PoseEstimator3d {
static constexpr units::second_t kBufferDuration = 1.5_s;
Odometry3d<WheelSpeeds, WheelPositions>& m_odometry;
// Diagonal of process noise covariance matrix Q
wpi::array<double, 4> m_q{wpi::empty_array};
frc::Matrixd<6, 6> m_visionK = frc::Matrixd<6, 6>::Zero();
// Kalman gain matrix K
Eigen::DiagonalMatrix<double, 6> m_vision_K =
Eigen::DiagonalMatrix<double, 6>::Zero();
// Maps timestamps to odometry-only pose estimates
TimeInterpolatableBuffer<Pose3d> m_odometryPoseBuffer{kBufferDuration};

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;

View File

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

View File

@@ -166,7 +166,13 @@ public final class Units {
* A unit of angular velocity equivalent to spinning at a rate of one {@link #Rotations Rotation}
* per {@link #Minute}. Motor spec sheets often list maximum speeds in terms of RPM.
*/
public static final AngularVelocityUnit RPM = Rotations.per(Minute);
public static final AngularVelocityUnit RotationsPerMinute = Rotations.per(Minute);
/**
* A unit of angular velocity equivalent to spinning at a rate of one {@link #Rotations Rotation}
* per {@link #Minute}. Motor spec sheets often list maximum speeds in terms of RPM.
*/
public static final AngularVelocityUnit RPM = RotationsPerMinute; // alias
/**
* The standard SI unit of angular velocity, equivalent to spinning at a rate of one {@link

View File

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