Compare commits

...

6 Commits

Author SHA1 Message Date
crueter
c89401250f [hal, wpilib] Usage Reporting: QFRCDashboard -> QDash (#8571) 2026-01-15 19:55:55 -07:00
Tyler Veness
8be7720a68 [sysid] Fix crash on partially empty raw data (#8572)
Fixes #8570.
2026-01-15 18:57:38 -07:00
Michael Lesirge
21b5389bbe [wpimath,cmd] Add multi tap boolean stream filter and multi tap trigger modifier (double tap detector) (#8307)
Add a simple tap counting filter for boolean streams. 

The filter activates when the input has risen (transitioned from false
to true, like when a button is tapped) the required number of times
within the time window after the first rising edge. Once activated, the
output remains true as long as the input is true. The tap count resets
when the time window expires or when the input goes false after
activation.

Example usage:
```java
    xbox.a()
      .multiPress(2, 0.2) // Detect a double tap within 0.2 seconds
      .onTrue(Commands.print("Double tapped A button"));
      
     xbox.y()
      .multiPress(2, 0.5) // Detect a double tap within 0.5 seconds
      .whileTrue(Commands.print("Y held after tap").repeatedly());
```

This is not a noise reduction and/or input smoothing filter, but it is
similar in usage to debounce, so I believe it could be considered a
filter, but am open to a better location.

I believe this would be a useful addition, as double/triple tapping a
button is a common control option in games, yet is not often utilized by
newer FRC teams. I believe adding it to WPILib in a standard way will
allow more teams to make the most out of their controls.
2026-01-14 20:22:07 -08:00
Joseph Eng
9e1258440b [wpimath] Fix Rotation3d interpolation and document extrinsic vs intrinsic (#8544)
Documents the extrinsic vs intrinsic semantics of `plus()` and
`minus()`. (`rotateBy()` was documented in [a previous
PR](https://github.com/wpilibsuite/allwpilib/pull/5508))
Fixes usage of `plus()` and `minus()` in `Rotation3d.interpolate()`.
(Fixes #8523)
Fixes incorrect usages of `plus()`, `minus()`, and `rotateBy()`
throughout `Odometry3d`.
Adds explanatory comments for some `plus()`, `minus()`, and `rotateBy()`
operations.
Fixes `TimeInterpolatableBuffer` not using twists for `Pose3d` (this was
just because I happened to notice it, it isn't really related to the PR)

To check all of our usages of `plus()`, `minus()`, and `rotateBy()`, I
marked them as deprecated, checked compile errors from `./gradlew
compileJava`, and then undeprecated them. You can see all of the spots
that showed up (at least on the Java side) by viewing the diff for
241109c.

I wanted to present this alternative to #8526 because the change has its
own quirks, there's little time before kickoff, and there would be no
code-side warning to teams (and mentors) already used to the current
behavior.
2026-01-14 20:16:24 -08:00
Dave Oleksy
812a1b8e1a [hal] Add 2026 REV products for usage reporting (#8567)
Adding REV Easy Swerve and MAXSpline Encoder to usage reporting.

Co-authored-by: Dave Oleksy <dave.oleksy@revrobotics.com>
2026-01-14 20:14:20 -08:00
Kevin-OConnor
18249badc0 Add 2026 game specifics (#8558)
Co-authored-by: Peter Johnson <johnson.peter@gmail.com>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
2026-01-12 19:07:51 -08:00
58 changed files with 2367 additions and 81 deletions

View File

@@ -17,13 +17,17 @@ public enum AprilTagFields {
/** 2025 Reefscape Welded (see TU 12). */
k2025ReefscapeWelded("2025-reefscape-welded.json"),
/** 2025 Reefscape AndyMark (see TU 12). */
k2025ReefscapeAndyMark("2025-reefscape-andymark.json");
k2025ReefscapeAndyMark("2025-reefscape-andymark.json"),
/** 2026 Rebuilt Welded. */
k2026RebuiltWelded("2026-rebuilt-welded.json"),
/** 2026 Rebuilt AndyMark. */
k2026RebuiltAndymark("2026-rebuilt-andymark.json");
/** Base resource directory. */
public static final String kBaseResourceDir = "/edu/wpi/first/apriltag/";
/** Alias to the current game. */
public static final AprilTagFields kDefaultField = k2025ReefscapeWelded;
public static final AprilTagFields kDefaultField = k2026RebuiltWelded;
/** Resource filename. */
public final String m_resourceFile;

View File

@@ -135,6 +135,8 @@ std::string_view GetResource_2023_chargedup_json();
std::string_view GetResource_2024_crescendo_json();
std::string_view GetResource_2025_reefscape_welded_json();
std::string_view GetResource_2025_reefscape_andymark_json();
std::string_view GetResource_2026_rebuilt_welded_json();
std::string_view GetResource_2026_rebuilt_andymark_json();
} // namespace frc
@@ -156,6 +158,12 @@ AprilTagFieldLayout AprilTagFieldLayout::LoadField(AprilTagField field) {
case AprilTagField::k2025ReefscapeAndyMark:
fieldString = GetResource_2025_reefscape_andymark_json();
break;
case AprilTagField::k2026RebuiltWelded:
fieldString = GetResource_2026_rebuilt_welded_json();
break;
case AprilTagField::k2026RebuiltAndyMark:
fieldString = GetResource_2026_rebuilt_andymark_json();
break;
case AprilTagField::kNumFields:
throw std::invalid_argument("Invalid Field");
}

View File

@@ -24,8 +24,12 @@ enum class AprilTagField {
k2025ReefscapeAndyMark,
/// 2025 Reefscape Welded (see TU12).
k2025ReefscapeWelded,
/// 2026 Rebuilt Andymark.
k2026RebuiltAndyMark,
/// 2026 Rebuilt Welded.
k2026RebuiltWelded,
/// Alias to the current game.
kDefaultField = k2025ReefscapeWelded,
kDefaultField = k2026RebuiltWelded,
// This is a placeholder for denoting the last supported field. This should
// always be the last entry in the enum and should not be used by users

View File

@@ -0,0 +1,33 @@
ID,X,Y,Z,Z-Rotation,X-Rotation
1,467.085,291.791,35,180,0
2,468.559,182.077,44.25,90,0
3,444.797,172.321,44.25,180,0
4,444.797,158.321,44.25,180,0
5,468.559,134.565,44.25,270,0
6,467.085,24.851,35,180,0
7,470.034,24.851,35,0,0
8,482.559,134.565,44.25,270,0
9,492.329,144.321,44.25,0,0
10,492.329,158.321,44.25,0,0
11,482.559,182.077,44.25,90,0
12,470.034,291.791,35,0,0
13,649.58,291.02,21.75,180,0
14,649.58,274.02,21.75,180,0
15,649.566,169.783,21.75,180,0
16,649.566,152.783,21.75,180,0
17,183.034,24.851,35,0,0
18,181.559,134.565,44.25,270,0
19,205.321,144.321,44.25,0,0
20,205.321,158.321,44.25,0,0
21,181.559,182.077,44.25,90,0
22,183.034,291.791,35,0,0
23,180.085,291.791,35,180,0
24,167.559,182.077,44.25,90,0
25,157.79,172.321,44.25,180,0
26,157.79,158.321,44.25,180,0
27,167.559,134.565,44.25,270,0
28,180.085,24.851,35,180,0
29,0.539,25.621,21.75,0,0
30,0.539,42.621,21.75,0,0
31,0.553,146.858,21.75,0,0
32,0.553,163.858,21.75,0,0
1 ID X Y Z Z-Rotation X-Rotation
2 1 467.085 291.791 35 180 0
3 2 468.559 182.077 44.25 90 0
4 3 444.797 172.321 44.25 180 0
5 4 444.797 158.321 44.25 180 0
6 5 468.559 134.565 44.25 270 0
7 6 467.085 24.851 35 180 0
8 7 470.034 24.851 35 0 0
9 8 482.559 134.565 44.25 270 0
10 9 492.329 144.321 44.25 0 0
11 10 492.329 158.321 44.25 0 0
12 11 482.559 182.077 44.25 90 0
13 12 470.034 291.791 35 0 0
14 13 649.58 291.02 21.75 180 0
15 14 649.58 274.02 21.75 180 0
16 15 649.566 169.783 21.75 180 0
17 16 649.566 152.783 21.75 180 0
18 17 183.034 24.851 35 0 0
19 18 181.559 134.565 44.25 270 0
20 19 205.321 144.321 44.25 0 0
21 20 205.321 158.321 44.25 0 0
22 21 181.559 182.077 44.25 90 0
23 22 183.034 291.791 35 0 0
24 23 180.085 291.791 35 180 0
25 24 167.559 182.077 44.25 90 0
26 25 157.79 172.321 44.25 180 0
27 26 157.79 158.321 44.25 180 0
28 27 167.559 134.565 44.25 270 0
29 28 180.085 24.851 35 180 0
30 29 0.539 25.621 21.75 0 0
31 30 0.539 42.621 21.75 0 0
32 31 0.553 146.858 21.75 0 0
33 32 0.553 163.858 21.75 0 0

View File

@@ -0,0 +1,584 @@
{
"tags": [
{
"ID": 1,
"pose": {
"translation": {
"x": 11.863959,
"y": 7.411491399999999,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 2,
"pose": {
"translation": {
"x": 11.9013986,
"y": 4.6247558,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 3,
"pose": {
"translation": {
"x": 11.2978438,
"y": 4.3769534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 4,
"pose": {
"translation": {
"x": 11.2978438,
"y": 4.0213534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 5,
"pose": {
"translation": {
"x": 11.9013986,
"y": 3.417951,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 6,
"pose": {
"translation": {
"x": 11.863959,
"y": 0.6312154,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 7,
"pose": {
"translation": {
"x": 11.9388636,
"y": 0.6312154,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 8,
"pose": {
"translation": {
"x": 12.2569986,
"y": 3.417951,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 9,
"pose": {
"translation": {
"x": 12.5051566,
"y": 3.6657534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 10,
"pose": {
"translation": {
"x": 12.5051566,
"y": 4.0213534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 11,
"pose": {
"translation": {
"x": 12.2569986,
"y": 4.6247558,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 12,
"pose": {
"translation": {
"x": 11.9388636,
"y": 7.411491399999999,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 13,
"pose": {
"translation": {
"x": 16.499332,
"y": 7.391907999999999,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 14,
"pose": {
"translation": {
"x": 16.499332,
"y": 6.960107999999999,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 15,
"pose": {
"translation": {
"x": 16.4989764,
"y": 4.3124882,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 16,
"pose": {
"translation": {
"x": 16.4989764,
"y": 3.8806881999999994,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 17,
"pose": {
"translation": {
"x": 4.6490636,
"y": 0.6312154,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 18,
"pose": {
"translation": {
"x": 4.6115986,
"y": 3.417951,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 19,
"pose": {
"translation": {
"x": 5.2151534,
"y": 3.6657534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 20,
"pose": {
"translation": {
"x": 5.2151534,
"y": 4.0213534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 21,
"pose": {
"translation": {
"x": 4.6115986,
"y": 4.6247558,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 22,
"pose": {
"translation": {
"x": 4.6490636,
"y": 7.411491399999999,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 23,
"pose": {
"translation": {
"x": 4.574159,
"y": 7.411491399999999,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 24,
"pose": {
"translation": {
"x": 4.2559986,
"y": 4.6247558,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 25,
"pose": {
"translation": {
"x": 4.007866,
"y": 4.3769534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 26,
"pose": {
"translation": {
"x": 4.007866,
"y": 4.0213534,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 27,
"pose": {
"translation": {
"x": 4.2559986,
"y": 3.417951,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 28,
"pose": {
"translation": {
"x": 4.574159,
"y": 0.6312154,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 29,
"pose": {
"translation": {
"x": 0.0136906,
"y": 0.6507734,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 30,
"pose": {
"translation": {
"x": 0.0136906,
"y": 1.0825734,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 31,
"pose": {
"translation": {
"x": 0.0140462,
"y": 3.7301932,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 32,
"pose": {
"translation": {
"x": 0.0140462,
"y": 4.1619931999999995,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
}
],
"field": {
"length": 16.518,
"width": 8.043
}
}

View File

@@ -0,0 +1,33 @@
ID,X,Y,Z,Z-Rotation,X-Rotation
1,467.637,292.314,35,180,0
2,469.111,182.6,44.25,90,0
3,445.349,172.844,44.25,180,0
4,445.349,158.844,44.25,180,0
5,469.111,135.088,44.25,270,0
6,467.637,25.374,35,180,0
7,470.586,25.374,35,0,0
8,483.111,135.088,44.25,270,0
9,492.881,144.844,44.25,0,0
10,492.881,158.844,44.25,0,0
11,483.111,182.6,44.25,90,0
12,470.586,292.314,35,0,0
13,650.918,291.469,21.75,180,0
14,650.918,274.469,21.75,180,0
15,650.904,170.219,21.75,180,0
16,650.904,153.219,21.75,180,0
17,183.586,25.374,35,0,0
18,182.111,135.088,44.25,270,0
19,205.873,144.844,44.25,0,0
20,205.873,158.844,44.25,0,0
21,182.111,182.6,44.25,90,0
22,183.586,292.314,35,0,0
23,180.637,292.314,35,180,0
24,168.111,182.6,44.25,90,0
25,158.341,172.844,44.25,180,0
26,158.341,158.844,44.25,180,0
27,168.111,135.088,44.25,270,0
28,180.637,25.374,35,180,0
29,0.305,26.219,21.75,0,0
30,0.305,43.219,21.75,0,0
31,0.318,147.469,21.75,0,0
32,0.318,164.469,21.75,0,0
1 ID X Y Z Z-Rotation X-Rotation
2 1 467.637 292.314 35 180 0
3 2 469.111 182.6 44.25 90 0
4 3 445.349 172.844 44.25 180 0
5 4 445.349 158.844 44.25 180 0
6 5 469.111 135.088 44.25 270 0
7 6 467.637 25.374 35 180 0
8 7 470.586 25.374 35 0 0
9 8 483.111 135.088 44.25 270 0
10 9 492.881 144.844 44.25 0 0
11 10 492.881 158.844 44.25 0 0
12 11 483.111 182.6 44.25 90 0
13 12 470.586 292.314 35 0 0
14 13 650.918 291.469 21.75 180 0
15 14 650.918 274.469 21.75 180 0
16 15 650.904 170.219 21.75 180 0
17 16 650.904 153.219 21.75 180 0
18 17 183.586 25.374 35 0 0
19 18 182.111 135.088 44.25 270 0
20 19 205.873 144.844 44.25 0 0
21 20 205.873 158.844 44.25 0 0
22 21 182.111 182.6 44.25 90 0
23 22 183.586 292.314 35 0 0
24 23 180.637 292.314 35 180 0
25 24 168.111 182.6 44.25 90 0
26 25 158.341 172.844 44.25 180 0
27 26 158.341 158.844 44.25 180 0
28 27 168.111 135.088 44.25 270 0
29 28 180.637 25.374 35 180 0
30 29 0.305 26.219 21.75 0 0
31 30 0.305 43.219 21.75 0 0
32 31 0.318 147.469 21.75 0 0
33 32 0.318 164.469 21.75 0 0

View File

@@ -0,0 +1,584 @@
{
"tags": [
{
"ID": 1,
"pose": {
"translation": {
"x": 11.8779798,
"y": 7.4247756,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 2,
"pose": {
"translation": {
"x": 11.9154194,
"y": 4.638039999999999,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 3,
"pose": {
"translation": {
"x": 11.3118646,
"y": 4.3902376,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 4,
"pose": {
"translation": {
"x": 11.3118646,
"y": 4.0346376,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 5,
"pose": {
"translation": {
"x": 11.9154194,
"y": 3.4312351999999997,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 6,
"pose": {
"translation": {
"x": 11.8779798,
"y": 0.6444996,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 7,
"pose": {
"translation": {
"x": 11.9528844,
"y": 0.6444996,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 8,
"pose": {
"translation": {
"x": 12.2710194,
"y": 3.4312351999999997,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 9,
"pose": {
"translation": {
"x": 12.519177399999998,
"y": 3.6790375999999996,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 10,
"pose": {
"translation": {
"x": 12.519177399999998,
"y": 4.0346376,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 11,
"pose": {
"translation": {
"x": 12.2710194,
"y": 4.638039999999999,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 12,
"pose": {
"translation": {
"x": 11.9528844,
"y": 7.4247756,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 13,
"pose": {
"translation": {
"x": 16.5333172,
"y": 7.4033126,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 14,
"pose": {
"translation": {
"x": 16.5333172,
"y": 6.9715126,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 15,
"pose": {
"translation": {
"x": 16.5329616,
"y": 4.3235626,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 16,
"pose": {
"translation": {
"x": 16.5329616,
"y": 3.8917626,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 17,
"pose": {
"translation": {
"x": 4.6630844,
"y": 0.6444996,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 18,
"pose": {
"translation": {
"x": 4.6256194,
"y": 3.4312351999999997,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 19,
"pose": {
"translation": {
"x": 5.229174199999999,
"y": 3.6790375999999996,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 20,
"pose": {
"translation": {
"x": 5.229174199999999,
"y": 4.0346376,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 21,
"pose": {
"translation": {
"x": 4.6256194,
"y": 4.638039999999999,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 22,
"pose": {
"translation": {
"x": 4.6630844,
"y": 7.4247756,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 23,
"pose": {
"translation": {
"x": 4.5881798,
"y": 7.4247756,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 24,
"pose": {
"translation": {
"x": 4.2700194,
"y": 4.638039999999999,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 25,
"pose": {
"translation": {
"x": 4.0218614,
"y": 4.3902376,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 26,
"pose": {
"translation": {
"x": 4.0218614,
"y": 4.0346376,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 27,
"pose": {
"translation": {
"x": 4.2700194,
"y": 3.4312351999999997,
"z": 1.12395
},
"rotation": {
"quaternion": {
"W": -0.7071067811865475,
"X": -0.0,
"Y": 0.0,
"Z": 0.7071067811865476
}
}
}
},
{
"ID": 28,
"pose": {
"translation": {
"x": 4.5881798,
"y": 0.6444996,
"z": 0.889
},
"rotation": {
"quaternion": {
"W": 6.123233995736766e-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 29,
"pose": {
"translation": {
"x": 0.0077469999999999995,
"y": 0.6659626,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 30,
"pose": {
"translation": {
"x": 0.0077469999999999995,
"y": 1.0977626,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 31,
"pose": {
"translation": {
"x": 0.0080772,
"y": 3.7457125999999996,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 32,
"pose": {
"translation": {
"x": 0.0080772,
"y": 4.1775126,
"z": 0.55245
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
}
],
"field": {
"length": 16.541,
"width": 8.069
}
}

View File

@@ -17,12 +17,13 @@ public enum Fields {
k2022RapidReact("2022-rapidreact.json"),
k2023ChargedUp("2023-chargedup.json"),
k2024Crescendo("2024-crescendo.json"),
k2025Reefscape("2025-reefscape.json");
k2025Reefscape("2025-reefscape.json"),
k2026Rebuilt("2026-rebuilt.json");
public static final String kBaseResourceDir = "/edu/wpi/first/fields/";
/** Alias to the current game. */
public static final Fields kDefaultField = k2025Reefscape;
public static final Fields kDefaultField = k2026Rebuilt;
public final String m_resourceFile;

View File

@@ -17,10 +17,12 @@
#include "fields/2023-chargedup.h"
#include "fields/2024-crescendo.h"
#include "fields/2025-reefscape.h"
#include "fields/2026-rebuilt.h"
using namespace fields;
static const Field kFields[] = {
{"2026 Rebuilt", GetResource_2026_rebuilt_json, GetResource_2026_field_png},
{"2025 Reefscape", GetResource_2025_reefscape_json,
GetResource_2025_field_png},
{"2024 Crescendo", GetResource_2024_crescendo_json,

View File

@@ -0,0 +1,12 @@
// 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 <string_view>
namespace fields {
std::string_view GetResource_2026_rebuilt_json();
std::string_view GetResource_2026_field_png();
} // namespace fields

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

View File

@@ -0,0 +1,19 @@
{
"game": "Rebuilt",
"field-image": "2026-field.png",
"field-corners": {
"top-left": [
245,
118
],
"bottom-right": [
3942,
1914
]
},
"field-size": [
54.269,
26.474
],
"field-unit": "foot"
}

View File

@@ -343,7 +343,7 @@ static bool InputPose(frc::Pose2d* pose) {
}
FieldInfo::FieldInfo(Storage& storage)
: m_builtin{storage.GetString("builtin", "2025 Reefscape")},
: m_builtin{storage.GetString("builtin", "2026 Rebuilt")},
m_filename{storage.GetString("image")},
m_width{storage.GetFloat("width", kDefaultWidth.to<float>())},
m_height{storage.GetFloat("height", kDefaultHeight.to<float>())},

View File

@@ -36,6 +36,7 @@ kRobotDriveSwerve_YAGSL = 15
kRobotDriveSwerve_CTRE = 16
kRobotDriveSwerve_MaxSwerve = 17
kRobotDriveSwerve_AdvantageKit = 18
kRobotDriveSwerve_EasySwerve = 19
kDriverStationCIO_Analog = 1
kDriverStationCIO_DigitalIn = 2
kDriverStationCIO_DigitalOut = 3
@@ -69,7 +70,7 @@ kDashboard_Shuffleboard = 4
kDashboard_Elastic = 5
kDashboard_LabVIEW = 6
kDashboard_AdvantageScope = 7
kDashboard_QFRCDashboard = 8
kDashboard_QDash = 8
kDashboard_FRCWebComponents = 9
kDataLogLocation_Onboard = 1
kDataLogLocation_USB = 2

View File

@@ -134,3 +134,4 @@ kResourceType_PoseEstimator3d = 132
kResourceType_LinearSystemLoop = 133
kResourceType_LumynLabs_ConnectorX = 134
kResourceType_LumynLabs_ConnectorXAnimate = 135
kResourceType_RevMAXSplineEncoder = 136

View File

@@ -291,6 +291,8 @@ public final class FRCNetComm {
public static final int kResourceType_LumynLabs_ConnectorX = 134;
/** kResourceType_LumynLabs_ConnectorXAnimate = 135. */
public static final int kResourceType_LumynLabs_ConnectorXAnimate = 135;
/** kResourceType_RevMAXSplineEncoder = 136. */
public static final int kResourceType_RevMAXSplineEncoder = 136;
}
/**
@@ -377,6 +379,8 @@ public final class FRCNetComm {
public static final int kRobotDriveSwerve_MaxSwerve = 17;
/** kRobotDriveSwerve_AdvantageKit = 18. */
public static final int kRobotDriveSwerve_AdvantageKit = 18;
/** kRobotDriveSwerve_EasySwerve = 19. */
public static final int kRobotDriveSwerve_EasySwerve = 19;
/** kDriverStationCIO_Analog = 1. */
public static final int kDriverStationCIO_Analog = 1;
/** kDriverStationCIO_DigitalIn = 2. */
@@ -443,8 +447,8 @@ public final class FRCNetComm {
public static final int kDashboard_LabVIEW = 6;
/** kDashboard_AdvantageScope = 7. */
public static final int kDashboard_AdvantageScope = 7;
/** kDashboard_QFRCDashboard = 8. */
public static final int kDashboard_QFRCDashboard = 8;
/** kDashboard_QDash = 8. */
public static final int kDashboard_QDash = 8;
/** kDashboard_FRCWebComponents = 9. */
public static final int kDashboard_FRCWebComponents = 9;
/** kDataLogLocation_Onboard = 1. */

View File

@@ -187,6 +187,7 @@ namespace HALUsageReporting {
kResourceType_LinearSystemLoop = 133,
kResourceType_LumynLabs_ConnectorX = 134,
kResourceType_LumynLabs_ConnectorXAnimate = 135,
kResourceType_RevMAXSplineEncoder = 136,
};
enum tInstances : int32_t {
kLanguage_LabVIEW = 1,
@@ -227,6 +228,7 @@ namespace HALUsageReporting {
kRobotDriveSwerve_CTRE = 16,
kRobotDriveSwerve_MaxSwerve = 17,
kRobotDriveSwerve_AdvantageKit = 18,
kRobotDriveSwerve_EasySwerve = 19,
kDriverStationCIO_Analog = 1,
kDriverStationCIO_DigitalIn = 2,
kDriverStationCIO_DigitalOut = 3,
@@ -260,7 +262,7 @@ namespace HALUsageReporting {
kDashboard_Elastic = 5,
kDashboard_LabVIEW = 6,
kDashboard_AdvantageScope = 7,
kDashboard_QFRCDashboard = 8,
kDashboard_QDash = 8,
kDashboard_FRCWebComponents = 9,
kDataLogLocation_Onboard = 1,
kDataLogLocation_USB = 2,

View File

@@ -156,6 +156,7 @@ typedef enum
kResourceType_LinearSystemLoop = 133,
kResourceType_LumynLabs_ConnectorX = 134,
kResourceType_LumynLabs_ConnectorXAnimate = 135,
kResourceType_RevMAXSplineEncoder = 136,
// kResourceType_MaximumID = 255,
} tResourceType;
@@ -200,6 +201,7 @@ typedef enum
kRobotDriveSwerve_CTRE = 16,
kRobotDriveSwerve_MaxSwerve = 17,
kRobotDriveSwerve_AdvantageKit = 18,
kRobotDriveSwerve_EasySwerve = 19,
kDriverStationCIO_Analog = 1,
kDriverStationCIO_DigitalIn = 2,
kDriverStationCIO_DigitalOut = 3,
@@ -233,7 +235,7 @@ typedef enum
kDashboard_Elastic = 5,
kDashboard_LabVIEW = 6,
kDashboard_AdvantageScope = 7,
kDashboard_QFRCDashboard = 8,
kDashboard_QDash = 8,
kDashboard_FRCWebComponents = 9,
kDataLogLocation_Onboard = 1,
kDataLogLocation_USB = 2,

View File

@@ -57,7 +57,8 @@ static double Lerp(units::second_t time,
*/
static std::vector<PreparedData> ConvertToPrepared(const MotorData& data) {
std::vector<PreparedData> prepared;
// assume we've selected down to a single contiguous run by this point
// Assume we've selected down to a single contiguous run by this point
auto run = data.runs[0];
for (int i = 0; i < static_cast<int>(run.voltage.size()) - 1; ++i) {
@@ -101,7 +102,7 @@ static void CopyRawData(wpi::StringMap<MotorData>* dataset) {
}
/**
* Assigns the combines the various datasets into a single one for analysis.
* Combines the various datasets into a single one for analysis.
*
* @param slowForward The slow forward dataset
* @param slowBackward The slow backward dataset
@@ -127,6 +128,28 @@ void AnalysisManager::PrepareGeneralData() {
// Convert data to PreparedData structs
for (auto& it : m_data.motorData) {
auto key = it.first;
// Assume we've selected down to a single contiguous run by this point
auto run = m_data.motorData[key].runs[0];
// Ensure data has at least two samples in it or linear interpolation within
// ConvertToPrepared() will fail
if (run.voltage.size() < 2) {
throw sysid::InvalidDataError(fmt::format(
"{} data has {} voltage samples and at least 2 are required.", key,
run.voltage.size()));
}
if (run.position.size() < 2) {
throw sysid::InvalidDataError(fmt::format(
"{} data has {} position samples and at least 2 are required.", key,
run.position.size()));
}
if (run.velocity.size() < 2) {
throw sysid::InvalidDataError(fmt::format(
"{} data has {} velocity samples and at least 2 are required.", key,
run.velocity.size()));
}
preparedData[key] = ConvertToPrepared(m_data.motorData[key]);
WPI_INFO(m_logger, "SAMPLES {}", preparedData[key].size());
}

View File

@@ -7,6 +7,7 @@ package edu.wpi.first.wpilibj2.command.button;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.math.filter.Debouncer;
import edu.wpi.first.math.filter.EdgeCounterFilter;
import edu.wpi.first.wpilibj.event.EventLoop;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
@@ -287,4 +288,31 @@ public class Trigger implements BooleanSupplier {
}
});
}
/**
* Creates a new multi-press trigger from this trigger - it will become active when this trigger
* has been activated the required number of times within the specified time window.
*
* <p>This is useful for implementing "double-click" style functionality.
*
* <p>Input for this must be stable, consider using a Debouncer before this to avoid counting
* noise as multiple presses.
*
* @param requiredPresses The number of presses required.
* @param windowTime The number of seconds in which the presses must occur.
* @return The multi-press trigger.
*/
public Trigger multiPress(int requiredPresses, double windowTime) {
return new Trigger(
m_loop,
new BooleanSupplier() {
final EdgeCounterFilter m_edgeCounterFilter =
new EdgeCounterFilter(requiredPresses, windowTime);
@Override
public boolean getAsBoolean() {
return m_edgeCounterFilter.calculate(m_condition.getAsBoolean());
}
});
}
}

View File

@@ -7,6 +7,7 @@
#include <utility>
#include <frc/filter/Debouncer.h>
#include <frc/filter/EdgeCountFilter.h>
#include "frc2/command/CommandPtr.h"
@@ -184,6 +185,14 @@ Trigger Trigger::Debounce(units::second_t debounceTime,
});
}
Trigger Trigger::MultiPress(int requiredPresses, units::second_t windowTime) {
return Trigger(m_loop,
[filter = frc::EdgeCounterFilter(requiredPresses, windowTime),
condition = m_condition]() mutable {
return filter.Calculate(condition());
});
}
bool Trigger::Get() const {
return m_condition();
}

View File

@@ -284,6 +284,22 @@ class Trigger {
frc::Debouncer::DebounceType type =
frc::Debouncer::DebounceType::kRising);
/**
* Creates a new multi-press trigger from this trigger - it will become active
* when this trigger has been activated the required number of times within
* the specified time window.
*
* <p>This is useful for implementing "double-click" style functionality.
*
* <p>Input for this must be stable, consider using a Debouncer before this to
* avoid counting noise as multiple presses.
*
* @param requiredPresses The number of presses required.
* @param windowTime The time in which the presses must occur.
* @return The multi-press trigger.
*/
Trigger MultiPress(int requiredPresses, units::second_t windowTime);
/**
* Returns the current state of this trigger.
*

View File

@@ -306,10 +306,11 @@ RobotBase::RobotBase() {
HAL_Report(HALUsageReporting::kResourceType_Dashboard,
HALUsageReporting::kDashboard_AdvantageScope);
m_dashboardDetected = true;
} else if (event.GetConnectionInfo()->remote_id.starts_with(
} else if (event.GetConnectionInfo()->remote_id.starts_with("QDash") ||
event.GetConnectionInfo()->remote_id.starts_with(
"QFRCDashboard")) {
HAL_Report(HALUsageReporting::kResourceType_Dashboard,
HALUsageReporting::kDashboard_QFRCDashboard);
HALUsageReporting::kDashboard_QDash);
m_dashboardDetected = true;
} else if (event.GetConnectionInfo()->remote_id.starts_with(
"FRC Web Components")) {

View File

@@ -211,9 +211,9 @@ public abstract class RobotBase implements AutoCloseable {
HAL.report(
tResourceType.kResourceType_Dashboard, tInstances.kDashboard_AdvantageScope);
m_dashboardDetected = true;
} else if (event.connInfo.remote_id.startsWith("QFRCDashboard")) {
HAL.report(
tResourceType.kResourceType_Dashboard, tInstances.kDashboard_QFRCDashboard);
} else if (event.connInfo.remote_id.startsWith("QDash")
|| event.connInfo.remote_id.startsWith("QFRCDashboard")) {
HAL.report(tResourceType.kResourceType_Dashboard, tInstances.kDashboard_QDash);
m_dashboardDetected = true;
} else if (event.connInfo.remote_id.startsWith("FRC Web Components")) {
HAL.report(

View File

@@ -0,0 +1,120 @@
// 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.math.filter;
import edu.wpi.first.math.MathSharedStore;
/**
* A rising edge counter for boolean streams. Requires that the boolean change value to true for a
* specified number of times within a specified time window after the first rising edge before the
* filtered value changes.
*
* <p>The filter activates when the input has risen (transitioned from false to true) the required
* number of times within the time window. Once activated, the output remains true as long as the
* input is true. The edge count resets when the time window expires or when the input goes false
* after activation.
*
* <p>Input must be stable; consider using a Debouncer before this filter to avoid counting noise as
* multiple edges.
*/
public class EdgeCounterFilter {
private int m_requiredEdges;
private double m_windowTimeSeconds;
private double m_firstEdgeTimeSeconds;
private int m_currentCount;
private boolean m_lastInput;
/**
* Creates a new EdgeCounterFilter.
*
* @param requiredEdges The number of rising edges required before the output goes true.
* @param windowTime The maximum number of seconds in which all required edges must occur after
* the first rising edge.
*/
public EdgeCounterFilter(int requiredEdges, double windowTime) {
m_requiredEdges = requiredEdges;
m_windowTimeSeconds = windowTime;
resetTimer();
}
private void resetTimer() {
m_firstEdgeTimeSeconds = MathSharedStore.getTimestamp();
}
private boolean hasElapsed() {
return MathSharedStore.getTimestamp() - m_firstEdgeTimeSeconds >= m_windowTimeSeconds;
}
/**
* Applies the edge counter filter to the input stream.
*
* @param input The current value of the input stream.
* @return True if the required number of edges have occurred within the time window and the input
* is currently true; false otherwise.
*/
public boolean calculate(boolean input) {
boolean enoughEdges = m_currentCount >= m_requiredEdges;
boolean expired = hasElapsed() && !enoughEdges;
boolean activationEnded = !input && enoughEdges;
if (expired || activationEnded) {
m_currentCount = 0;
}
if (input && !m_lastInput) {
if (m_currentCount == 0) {
resetTimer(); // Start timer on first rising edge
}
m_currentCount++;
}
m_lastInput = input;
return input && m_currentCount >= m_requiredEdges;
}
/**
* Sets the time window duration.
*
* @param windowTime The maximum window of seconds in which all required edges must occur after
* the first rising edge.
*/
public void setWindowTime(double windowTime) {
m_windowTimeSeconds = windowTime;
}
/**
* Gets the time window duration.
*
* @return The maximum window of seconds in which all required edges must occur after the first
* rising edge.
*/
public double getWindowTime() {
return m_windowTimeSeconds;
}
/**
* Sets the required number of edges.
*
* @param requiredEdges The number of rising edges required before the output goes true.
*/
public void setRequiredEdges(int requiredEdges) {
m_requiredEdges = requiredEdges;
}
/**
* Gets the required number of edges.
*
* @return The number of rising edges required before the output goes true.
*/
public int getRequiredEdges() {
return m_requiredEdges;
}
}

View File

@@ -16,7 +16,7 @@ public class CoordinateSystem {
private static final CoordinateSystem m_ned =
new CoordinateSystem(CoordinateAxis.N(), CoordinateAxis.E(), CoordinateAxis.D());
// Rotation from this coordinate system to NWU coordinate system
// Rotation from this coordinate system to NWU coordinate system when applied extrinsically
private final Rotation3d m_rotation;
/**
@@ -85,7 +85,8 @@ public class CoordinateSystem {
*/
public static Translation3d convert(
Translation3d translation, CoordinateSystem from, CoordinateSystem to) {
return translation.rotateBy(from.m_rotation.minus(to.m_rotation));
// Convert to NWU, then convert to the new coordinate system
return translation.rotateBy(from.m_rotation).rotateBy(to.m_rotation.unaryMinus());
}
/**
@@ -98,7 +99,8 @@ public class CoordinateSystem {
*/
public static Rotation3d convert(
Rotation3d rotation, CoordinateSystem from, CoordinateSystem to) {
return rotation.rotateBy(from.m_rotation.minus(to.m_rotation));
// Convert to NWU, then convert to the new coordinate system
return rotation.rotateBy(from.m_rotation).rotateBy(to.m_rotation.unaryMinus());
}
/**
@@ -124,9 +126,23 @@ public class CoordinateSystem {
*/
public static Transform3d convert(
Transform3d transform, CoordinateSystem from, CoordinateSystem to) {
var coordRot = from.m_rotation.minus(to.m_rotation);
// coordRot is the rotation that converts between the coordinate systems when applied
// extrinsically. It first converts to NWU, then converts to the new coordinate system.
var coordRot = from.m_rotation.rotateBy(to.m_rotation.unaryMinus());
// The new rotation is the extrinsic rotation from convert(zero) to
// convert(transformRot). That is, applying convertedRot extrinsically to
// convert(zero) produces convert(transformRot). In the below snippet, we
// use matrix notation, so rotA rotB applies rotA extrinsically to rotB.
//
// convertedRot convert(zero) = convert(transformRot)
// convertedRot = convert(transformRot) convert(zero)⁻¹
// = (coordRot transformRot) (coordRot zero)⁻¹
// = (coordRot transformRot) coordRot⁻¹
//
// In code, the equivalent for rotA rotB is rotB.rotateBy(rotA) (note the
// change in order), and the equivalent for rot⁻¹ is rot.unaryMinus().
return new Transform3d(
convert(transform.getTranslation(), from, to),
coordRot.unaryMinus().plus(transform.getRotation().rotateBy(coordRot)));
coordRot.unaryMinus().rotateBy(transform.getRotation().rotateBy(coordRot)));
}
}

View File

@@ -220,7 +220,7 @@ public class Pose2d implements Interpolatable<Pose2d>, ProtobufSerializable, Str
public Pose2d transformBy(Transform2d other) {
return new Pose2d(
m_translation.plus(other.getTranslation().rotateBy(m_rotation)),
other.getRotation().plus(m_rotation));
other.getRotation().rotateBy(m_rotation));
}
/**

View File

@@ -254,9 +254,12 @@ public class Pose3d implements Interpolatable<Pose3d>, ProtobufSerializable, Str
* @return The transformed pose.
*/
public Pose3d transformBy(Transform3d other) {
// Rotating the transform's rotation by the pose's rotation extrinsically is equivalent to
// rotating the pose's rotation by the transform's rotation intrinsically. (We define transforms
// as being applied intrinsically.)
return new Pose3d(
m_translation.plus(other.getTranslation().rotateBy(m_rotation)),
other.getRotation().plus(m_rotation));
other.getRotation().rotateBy(m_rotation));
}
/**

View File

@@ -275,6 +275,17 @@ public class Rotation2d
m_cos * other.m_cos - m_sin * other.m_sin, m_cos * other.m_sin + m_sin * other.m_cos);
}
/**
* Returns the current rotation relative to the given rotation.
*
* @param other The rotation describing the orientation of the new coordinate frame that the
* current rotation will be converted into.
* @return The current rotation relative to the new orientation of the coordinate frame.
*/
public Rotation2d relativeTo(Rotation2d other) {
return rotateBy(other.unaryMinus());
}
/**
* Returns matrix representation of this rotation.
*

View File

@@ -26,7 +26,49 @@ import edu.wpi.first.util.protobuf.ProtobufSerializable;
import edu.wpi.first.util.struct.StructSerializable;
import java.util.Objects;
/** A rotation in a 3D coordinate frame represented by a quaternion. */
/**
* A rotation in a 3D coordinate frame, represented by a quaternion. Note that unlike 2D rotations,
* 3D rotations are not always commutative, so changing the order of rotations changes the result.
*
* <p>As an example of the order of rotations mattering, suppose we have a card that looks like
* this:
*
* <pre>
*   ┌───┐ ┌───┐
* │ X │ │ x │
* Front: │ | │ Back: │ · │
* │ | │ │ · │
* └───┘ └───┘
* </pre>
*
* <p>If we rotate 90º clockwise around the axis into the page, then flip around the left/right
* axis, we get one result:
*
* <pre>
*  ┌───┐
* │ X │ ┌───────┐ ┌───────┐
* │ | │ → │------X│ → │······x│
* │ | │ └───────┘ └───────┘
* └───┘
* </pre>
*
* <p>If we flip around the left/right axis, then rotate 90º clockwise around the axis into the
* page, we get a different result:
*
* <pre>
*   ┌───┐ ┌───┐
* │ X │ │ · │ ┌───────┐
* │ | │ → │ · │ → │x······│
* │ | │ │ x │ └───────┘
* └───┘ └───┘
* </pre>
*
* <p>Because order matters for 3D rotations, we need to distinguish between <em>extrinsic</em>
* rotations and <em>intrinsic</em> rotations. Rotating extrinsically means rotating around the
* global axes, while rotating intrinsically means rotating from the perspective of the other
* rotation. A neat property is that applying a series of rotations extrinsically is the same as
* applying the same series in the opposite order intrinsically.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Rotation3d
@@ -286,9 +328,17 @@ public class Rotation3d
}
/**
* Adds two rotations together.
* Adds two rotations together. The other rotation is applied extrinsically to this rotation,
* which is equivalent to this rotation being applied intrinsically to the other rotation. See the
* class comment for definitions of extrinsic and intrinsic rotations.
*
* @param other The rotation to add.
* <p>Note that {@code a.minus(b).plus(b)} always equals {@code a}, but {@code b.plus(a.minus(b))}
* sometimes doesn't. To apply a rotation offset, use either {@code offset =
* measurement.unaryMinus().plus(actual); newAngle = angle.plus(offset);} or {@code offset =
* actual.minus(measurement); newAngle = offset.plus(angle);}, depending on how the corrected
* angle needs to change as the input angle changes.
*
* @param other The rotation to add (applied extrinsically).
* @return The sum of the two rotations.
*/
public Rotation3d plus(Rotation3d other) {
@@ -296,10 +346,19 @@ public class Rotation3d
}
/**
* Subtracts the new rotation from the current rotation and returns the new rotation.
* Subtracts the other rotation from the current rotation and returns the new rotation. The new
* rotation is from the perspective of the other rotation (like {@link Pose3d#minus}), so it needs
* to be applied intrinsically. See the class comment for definitions of extrinsic and intrinsic
* rotations.
*
* <p>Note that {@code a.minus(b).plus(b)} always equals {@code a}, but {@code b.plus(a.minus(b))}
* sometimes doesn't. To apply a rotation offset, use either {@code offset =
* measurement.unaryMinus().plus(actual); newAngle = angle.plus(offset);} or {@code offset =
* actual.minus(measurement); newAngle = offset.plus(angle);}, depending on how the corrected
* angle needs to change as the input angle changes.
*
* @param other The rotation to subtract.
* @return The difference between the two rotations.
* @return The difference between the two rotations, from the perspective of the other rotation.
*/
public Rotation3d minus(Rotation3d other) {
return rotateBy(other.unaryMinus());
@@ -358,6 +417,24 @@ public class Rotation3d
return new Rotation3d(other.m_q.times(m_q));
}
/**
* Returns the current rotation relative to the given rotation. Because the result is in the
* perspective of the given rotation, it must be applied intrinsically. See the class comment for
* definitions of extrinsic and intrinsic rotations.
*
* @param other The rotation describing the orientation of the new coordinate frame that the
* current rotation will be converted into.
* @return The current rotation relative to the new orientation of the coordinate frame.
*/
public Rotation3d relativeTo(Rotation3d other) {
// To apply a quaternion intrinsically, we must right-multiply by that quaternion.
// Therefore, "this_q relative to other_q" is the q such that other_q q = this_q:
//
// other_q q = this_q
// q = other_q⁻¹ this_q
return new Rotation3d(other.m_q.inverse().times(m_q));
}
/**
* Returns the quaternion representation of the Rotation3d.
*
@@ -562,7 +639,7 @@ public class Rotation3d
@Override
public Rotation3d interpolate(Rotation3d endValue, double t) {
return plus(endValue.minus(this).times(MathUtil.clamp(t, 0, 1)));
return endValue.minus(this).times(MathUtil.clamp(t, 0, 1)).plus(this);
}
/** Rotation3d protobuf for serialization. */

View File

@@ -36,15 +36,14 @@ public class Transform2d implements ProtobufSerializable, StructSerializable {
* @param last The final pose for the transformation.
*/
public Transform2d(Pose2d initial, Pose2d last) {
// We are rotating the difference between the translations
// using a clockwise rotation matrix. This transforms the global
// delta into a local delta (relative to the initial pose).
// To transform the global translation delta to be relative to the initial
// pose, rotate by the inverse of the initial pose's orientation.
m_translation =
last.getTranslation()
.minus(initial.getTranslation())
.rotateBy(initial.getRotation().unaryMinus());
m_rotation = last.getRotation().minus(initial.getRotation());
m_rotation = last.getRotation().relativeTo(initial.getRotation());
}
/**

View File

@@ -17,7 +17,10 @@ import edu.wpi.first.util.protobuf.ProtobufSerializable;
import edu.wpi.first.util.struct.StructSerializable;
import java.util.Objects;
/** Represents a transformation for a Pose3d in the pose's frame. */
/**
* Represents a transformation for a Pose3d in the pose's frame. Translation is applied before
* rotation. (The translation is applied in the pose's original frame, not the transformed frame.)
*/
public class Transform3d implements ProtobufSerializable, StructSerializable {
/**
* A preallocated Transform3d representing no transformation.
@@ -36,15 +39,14 @@ public class Transform3d implements ProtobufSerializable, StructSerializable {
* @param last The final pose for the transformation.
*/
public Transform3d(Pose3d initial, Pose3d last) {
// We are rotating the difference between the translations
// using a clockwise rotation matrix. This transforms the global
// delta into a local delta (relative to the initial pose).
// To transform the global translation delta to be relative to the initial
// pose, rotate by the inverse of the initial pose's orientation.
m_translation =
last.getTranslation()
.minus(initial.getTranslation())
.rotateBy(initial.getRotation().unaryMinus());
m_rotation = last.getRotation().minus(initial.getRotation());
m_rotation = last.getRotation().relativeTo(initial.getRotation());
}
/**

View File

@@ -24,7 +24,10 @@ public class Odometry<T> {
private Pose2d m_poseMeters;
private Rotation2d m_gyroOffset;
// Always equal to m_poseMeters.getRotation()
private Rotation2d m_previousAngle;
private final T m_previousWheelPositions;
/**
@@ -42,7 +45,7 @@ public class Odometry<T> {
Pose2d initialPoseMeters) {
m_kinematics = kinematics;
m_poseMeters = initialPoseMeters;
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_poseMeters.getRotation());
m_previousAngle = m_poseMeters.getRotation();
m_previousWheelPositions = m_kinematics.copy(wheelPositions);
}
@@ -60,7 +63,7 @@ public class Odometry<T> {
public void resetPosition(Rotation2d gyroAngle, T wheelPositions, Pose2d poseMeters) {
m_poseMeters = poseMeters;
m_previousAngle = m_poseMeters.getRotation();
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_poseMeters.getRotation());
m_kinematics.copyInto(wheelPositions, m_previousWheelPositions);
}
@@ -70,7 +73,10 @@ public class Odometry<T> {
* @param poseMeters The pose to reset to.
*/
public void resetPose(Pose2d poseMeters) {
m_gyroOffset = m_gyroOffset.plus(poseMeters.getRotation().minus(m_poseMeters.getRotation()));
m_gyroOffset =
m_gyroOffset
.rotateBy(m_poseMeters.getRotation().unaryMinus())
.rotateBy(poseMeters.getRotation());
m_poseMeters = poseMeters;
m_previousAngle = m_poseMeters.getRotation();
}
@@ -90,7 +96,8 @@ public class Odometry<T> {
* @param rotation The rotation to reset to.
*/
public void resetRotation(Rotation2d rotation) {
m_gyroOffset = m_gyroOffset.plus(rotation.minus(m_poseMeters.getRotation()));
m_gyroOffset =
m_gyroOffset.rotateBy(m_poseMeters.getRotation().unaryMinus()).rotateBy(rotation);
m_poseMeters = new Pose2d(m_poseMeters.getTranslation(), rotation);
m_previousAngle = m_poseMeters.getRotation();
}
@@ -115,7 +122,7 @@ public class Odometry<T> {
* @return The new pose of the robot.
*/
public Pose2d update(Rotation2d gyroAngle, T wheelPositions) {
var angle = gyroAngle.plus(m_gyroOffset);
var angle = gyroAngle.rotateBy(m_gyroOffset);
var twist = m_kinematics.toTwist2d(m_previousWheelPositions, wheelPositions);
twist.dtheta = angle.minus(m_previousAngle).getRadians();

View File

@@ -32,8 +32,15 @@ public class Odometry3d<T> {
private final Kinematics<?, T> m_kinematics;
private Pose3d m_poseMeters;
// Applying a rotation intrinsically to the measured gyro angle should cause the corrected angle
// to be rotated intrinsically in the same way, so the measured gyro angle must be applied
// intrinsically. This is equivalent to applying the offset extrinsically to the measured gyro
// angle.
private Rotation3d m_gyroOffset;
// Always equal to m_poseMeters.getRotation()
private Rotation3d m_previousAngle;
private final T m_previousWheelPositions;
/**
@@ -51,7 +58,9 @@ public class Odometry3d<T> {
Pose3d initialPoseMeters) {
m_kinematics = kinematics;
m_poseMeters = initialPoseMeters;
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
// When applied extrinsically, m_gyroOffset cancels the current gyroAngle and
// then rotates to m_poseMeters.getRotation()
m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_poseMeters.getRotation());
m_previousAngle = m_poseMeters.getRotation();
m_previousWheelPositions = m_kinematics.copy(wheelPositions);
}
@@ -69,7 +78,9 @@ public class Odometry3d<T> {
public void resetPosition(Rotation3d gyroAngle, T wheelPositions, Pose3d poseMeters) {
m_poseMeters = poseMeters;
m_previousAngle = m_poseMeters.getRotation();
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
// When applied extrinsically, m_gyroOffset cancels the current gyroAngle and
// then rotates to m_poseMeters.getRotation()
m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_poseMeters.getRotation());
m_kinematics.copyInto(wheelPositions, m_previousWheelPositions);
}
@@ -79,7 +90,11 @@ public class Odometry3d<T> {
* @param poseMeters The pose to reset to.
*/
public void resetPose(Pose3d poseMeters) {
m_gyroOffset = m_gyroOffset.plus(poseMeters.getRotation().minus(m_poseMeters.getRotation()));
// Cancel the previous m_pose.Rotation() and then rotate to the new angle
m_gyroOffset =
m_gyroOffset
.rotateBy(m_poseMeters.getRotation().unaryMinus())
.rotateBy(poseMeters.getRotation());
m_poseMeters = poseMeters;
m_previousAngle = m_poseMeters.getRotation();
}
@@ -99,7 +114,9 @@ public class Odometry3d<T> {
* @param rotation The rotation to reset to.
*/
public void resetRotation(Rotation3d rotation) {
m_gyroOffset = m_gyroOffset.plus(rotation.minus(m_poseMeters.getRotation()));
// Cancel the previous m_pose.Rotation() and then rotate to the new angle
m_gyroOffset =
m_gyroOffset.rotateBy(m_poseMeters.getRotation().unaryMinus()).rotateBy(rotation);
m_poseMeters = new Pose3d(m_poseMeters.getTranslation(), rotation);
m_previousAngle = m_poseMeters.getRotation();
}
@@ -124,7 +141,7 @@ public class Odometry3d<T> {
* @return The new pose of the robot.
*/
public Pose3d update(Rotation3d gyroAngle, T wheelPositions) {
var angle = gyroAngle.plus(m_gyroOffset);
var angle = gyroAngle.rotateBy(m_gyroOffset);
var angle_difference = angle.minus(m_previousAngle).toVector();
var twist2d = m_kinematics.toTwist2d(m_previousWheelPositions, wheelPositions);

View File

@@ -0,0 +1,46 @@
// 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 "frc/filter/EdgeCountFilter.h"
#include "wpimath/MathShared.h"
using namespace frc;
EdgeCounterFilter::EdgeCounterFilter(int requiredEdges, units::second_t window)
: m_requiredEdges(requiredEdges), m_windowTime(window) {
ResetTimer();
}
void EdgeCounterFilter::ResetTimer() {
m_firstEdgeTime = wpi::math::MathSharedStore::GetTimestamp();
}
bool EdgeCounterFilter::HasElapsed() const {
return wpi::math::MathSharedStore::GetTimestamp() - m_firstEdgeTime >=
m_windowTime;
}
bool EdgeCounterFilter::Calculate(bool input) {
bool enoughEdges = m_currentCount >= m_requiredEdges;
bool expired = HasElapsed() && !enoughEdges;
bool activationEnded = !input && enoughEdges;
if (expired || activationEnded) {
m_currentCount = 0;
}
if (input && !m_lastInput) {
if (m_currentCount == 0) {
ResetTimer(); // Start timer on first rising edge
}
m_currentCount++;
}
m_lastInput = input;
return input && m_currentCount >= m_requiredEdges;
}

View File

@@ -0,0 +1,94 @@
// 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 <wpi/SymbolExports.h>
#include "units/time.h"
namespace frc {
/**
* A rising edge counter for boolean streams. Requires that the boolean change
* value to true for a specified number of times within a specified time window
* after the first rising edge before the filtered value changes.
*
* The filter activates when the input has risen (transitioned from false to
* true) the required number of times within the time window. Once activated,
* the output remains true as long as the input is true. The edge count resets
* when the time window expires or when the input goes false after activation.
*
* Input must be stable; consider using a Debouncer before this filter to avoid
* counting noise as multiple edges.
*/
class WPILIB_DLLEXPORT EdgeCounterFilter {
public:
/**
* Creates a new EdgeCounterFilter.
*
* @param requiredEdges The number of rising edges required before the output
* goes true.
* @param windowTime The maximum time window in which all required edges must
* occur after the first rising edge.
*/
explicit EdgeCounterFilter(int requiredEdges, units::second_t windowTime);
/**
* Applies the edge counter filter to the input stream.
*
* @param input The current value of the input stream.
* @return True if the required number of edges have occurred within the time
* window and the input is currently true; false otherwise.
*/
bool Calculate(bool input);
/**
* Sets the time window duration.
*
* @param windowTime The maximum time window in which all required edges must
* occur after the first rising edge.
*/
constexpr void SetWindowTime(units::second_t windowTime) {
m_windowTime = windowTime;
}
/**
* Gets the time window duration.
*
* @return The maximum time window in which all required edges must occur
* after the first rising edge.
*/
constexpr units::second_t GetWindowTime() const { return m_windowTime; }
/**
* Sets the required number of edges.
*
* @param requiredEdges The number of rising edges required before the output
* goes true.
*/
constexpr void SetRequiredEdges(int requiredEdges) {
m_requiredEdges = requiredEdges;
}
/**
* Gets the required number of edges.
*
* @return The number of rising edges required before the output goes true.
*/
constexpr int GetRequiredEdges() const { return m_requiredEdges; }
private:
int m_requiredEdges;
units::second_t m_windowTime;
units::second_t m_firstEdgeTime;
int m_currentCount = 0;
bool m_lastInput = false;
void ResetTimer();
bool HasElapsed() const;
};
} // namespace frc

View File

@@ -86,7 +86,8 @@ class WPILIB_DLLEXPORT CoordinateSystem {
constexpr static Translation3d Convert(const Translation3d& translation,
const CoordinateSystem& from,
const CoordinateSystem& to) {
return translation.RotateBy(from.m_rotation - to.m_rotation);
// Convert to NWU, then convert to the new coordinate system
return translation.RotateBy(from.m_rotation).RotateBy(-to.m_rotation);
}
/**
@@ -100,7 +101,8 @@ class WPILIB_DLLEXPORT CoordinateSystem {
constexpr static Rotation3d Convert(const Rotation3d& rotation,
const CoordinateSystem& from,
const CoordinateSystem& to) {
return rotation.RotateBy(from.m_rotation - to.m_rotation);
// Convert to NWU, then convert to the new coordinate system
return rotation.RotateBy(from.m_rotation).RotateBy(-to.m_rotation);
}
/**
@@ -129,14 +131,30 @@ class WPILIB_DLLEXPORT CoordinateSystem {
constexpr static Transform3d Convert(const Transform3d& transform,
const CoordinateSystem& from,
const CoordinateSystem& to) {
const auto coordRot = from.m_rotation - to.m_rotation;
// coordRot is the rotation that converts between the coordinate systems
// when applied extrinsically. It first converts to NWU, then converts to
// the new coordinate system.
const auto coordRot = from.m_rotation.RotateBy(-to.m_rotation);
// The new rotation is the extrinsic rotation from convert(zero) to
// convert(transformRot). That is, applying convertedRot extrinsically to
// convert(zero) produces convert(transformRot). In the below snippet, we
// use matrix notation, so rotA rotB applies rotA extrinsically to rotB.
//
// convertedRot convert(zero) = convert(transformRot)
// convertedRot = convert(transformRot) convert(zero)⁻¹
// = (coordRot transformRot) (coordRot zero)⁻¹
// = (coordRot transformRot) coordRot⁻¹
//
// In code, the equivalent for rotA rotB is rotB.RotateBy(rotA) (note the
// change in order), and the equivalent for rot⁻¹ is -rot.
return Transform3d{
Convert(transform.Translation(), from, to),
(-coordRot).RotateBy(transform.Rotation().RotateBy(coordRot))};
}
private:
// Rotation from this coordinate system to NWU coordinate system
// Rotation from this coordinate system to NWU coordinate system when applied
// extrinsically
Rotation3d m_rotation;
};

View File

@@ -320,7 +320,7 @@ constexpr Transform2d Pose2d::operator-(const Pose2d& other) const {
constexpr Pose2d Pose2d::TransformBy(const frc::Transform2d& other) const {
return {m_translation + (other.Translation().RotateBy(m_rotation)),
other.Rotation() + m_rotation};
other.Rotation().RotateBy(m_rotation)};
}
constexpr Pose2d Pose2d::RelativeTo(const Pose2d& other) const {

View File

@@ -392,8 +392,11 @@ constexpr Transform3d Pose3d::operator-(const Pose3d& other) const {
}
constexpr Pose3d Pose3d::TransformBy(const Transform3d& other) const {
// Rotating the transform's rotation by the pose's rotation extrinsically is
// equivalent to rotating the pose's rotation by the transform's rotation
// intrinsically. (We define transforms as being applied intrinsically.)
return {m_translation + (other.Translation().RotateBy(m_rotation)),
other.Rotation() + m_rotation};
other.Rotation().RotateBy(m_rotation)};
}
constexpr Pose3d Pose3d::RelativeTo(const Pose3d& other) const {

View File

@@ -198,6 +198,19 @@ class WPILIB_DLLEXPORT Rotation2d {
Cos() * other.Sin() + Sin() * other.Cos()};
}
/**
* Returns the current rotation relative to the given rotation.
*
* @param other The rotation describing the orientation of the new coordinate
* frame that the current rotation will be converted into.
*
* @return The current rotation relative to the new orientation of the
* coordinate frame.
*/
constexpr Rotation2d RelativeTo(const Rotation2d& other) const {
return RotateBy(-other);
}
/**
* Returns matrix representation of this rotation.
*/

View File

@@ -10,6 +10,7 @@
#include <Eigen/Core>
#include <fmt/format.h>
#include <gcem.hpp>
#include <wpi/MathExtras.h>
#include <wpi/SymbolExports.h>
#include <wpi/json_fwd.h>
@@ -24,7 +25,49 @@
namespace frc {
/**
* A rotation in a 3D coordinate frame represented by a quaternion.
* A rotation in a 3D coordinate frame, represented by a quaternion. Note that
* unlike 2D rotations, 3D rotations are not always commutative, so changing the
* order of rotations changes the result.
*
* As an example of the order of rotations mattering, suppose we have a card
* that looks like this:
*
* <pre>
*   ┌───┐ ┌───┐
* │ X │ │ x │
* Front: │ | │ Back: │ · │
* │ | │ │ · │
* └───┘ └───┘
* </pre>
*
* If we rotate 90º clockwise around the axis into the page, then flip around
* the left/right axis, we get one result:
*
* <pre>
*  ┌───┐
* │ X │ ┌───────┐ ┌───────┐
* │ | │ → │------X│ → │······x│
* │ | │ └───────┘ └───────┘
* └───┘
* </pre>
*
* If we flip around the left/right axis, then rotate 90º clockwise around the
* axis into the page, we get a different result:
*
* <pre>
*   ┌───┐ ┌───┐
* │ X │ │ · │ ┌───────┐
* │ | │ → │ · │ → │x······│
* │ | │ │ x │ └───────┘
* └───┘ └───┘
* </pre>
*
* Because order matters for 3D rotations, we need to distinguish between
* <em>extrinsic</em> rotations and <em>intrinsic</em> rotations. Rotating
* extrinsically means rotating around the global axes, while rotating
* intrinsically means rotating from the perspective of the other rotation. A
* neat property is that applying a series of rotations extrinsically is the
* same as applying the same series in the opposite order intrinsically.
*/
class WPILIB_DLLEXPORT Rotation3d {
public:
@@ -242,9 +285,18 @@ class WPILIB_DLLEXPORT Rotation3d {
: Rotation3d{0_rad, 0_rad, rotation.Radians()} {}
/**
* Adds two rotations together.
* Adds two rotations together. The other rotation is applied extrinsically to
* this rotation, which is equivalent to this rotation being applied
* intrinsically to the other rotation. See the class comment for definitions
* of extrinsic and intrinsic rotations.
*
* @param other The rotation to add.
* Note that `a - b + b` always equals `a`, but `b + (a - b)`
* sometimes doesn't. To apply a rotation offset, use either `offset =
* -measurement + actual; newAngle = angle + offset;` or `offset = actual -
* measurement; newAngle = offset + angle;`, depending on how the corrected
* angle needs to change as the input angle changes.
*
* @param other The rotation to add (applied extrinsically).
*
* @return The sum of the two rotations.
*/
@@ -254,11 +306,20 @@ class WPILIB_DLLEXPORT Rotation3d {
/**
* Subtracts the new rotation from the current rotation and returns the new
* rotation.
* rotation. The new rotation is from the perspective of the other rotation
* (like Pose3d::operator-), so it needs to be applied intrinsically. See the
* class comment for definitions of extrinsic and intrinsic rotations.
*
* Note that `a - b + b` always equals `a`, but `b + (a - b)` sometimes
* doesn't. To apply a rotation offset, use either `offset = -measurement +
* actual; newAngle = angle + offset;` or `offset = actual - measurement;
* newAngle = offset + angle;`, depending on how the corrected angle needs to
* change as the input angle changes.
*
* @param other The rotation to subtract.
*
* @return The difference between the two rotations.
* @return The difference between the two rotations, from the perspective of
* the other rotation.
*/
constexpr Rotation3d operator-(const Rotation3d& other) const {
return *this + -other;
@@ -323,6 +384,28 @@ class WPILIB_DLLEXPORT Rotation3d {
return Rotation3d{other.m_q * m_q};
}
/**
* Returns the current rotation relative to the given rotation. Because the
* result is in the perspective of the given rotation, it must be applied
* intrinsically. See the class comment for definitions of extrinsic and
* intrinsic rotations.
*
* @param other The rotation describing the orientation of the new coordinate
* frame that the current rotation will be converted into.
*
* @return The current rotation relative to the new orientation of the
* coordinate frame.
*/
constexpr Rotation3d RelativeTo(const Rotation3d& other) const {
// To apply a quaternion intrinsically, we must right-multiply by that
// quaternion. Therefore, "this_q relative to other_q" is the q such that
// other_q q = this_q:
//
// other_q q = this_q
// q = other_q⁻¹ this_q
return Rotation3d{other.m_q.Inverse() * m_q};
}
/**
* Returns the quaternion representation of the Rotation3d.
*/
@@ -449,5 +532,11 @@ void from_json(const wpi::json& json, Rotation3d& rotation);
} // namespace frc
template <>
constexpr frc::Rotation3d wpi::Lerp(const frc::Rotation3d& startValue,
const frc::Rotation3d& endValue, double t) {
return (endValue - startValue) * t + startValue;
}
#include "frc/geometry/proto/Rotation3dProto.h"
#include "frc/geometry/struct/Rotation3dStruct.h"

View File

@@ -167,13 +167,12 @@ class WPILIB_DLLEXPORT Transform2d {
namespace frc {
constexpr Transform2d::Transform2d(const Pose2d& initial, const Pose2d& final) {
// We are rotating the difference between the translations
// using a clockwise rotation matrix. This transforms the global
// delta into a local delta (relative to the initial pose).
// To transform the global translation delta to be relative to the initial
// pose, rotate by the inverse of the initial pose's orientation.
m_translation = (final.Translation() - initial.Translation())
.RotateBy(-initial.Rotation());
m_rotation = final.Rotation() - initial.Rotation();
m_rotation = final.Rotation().RelativeTo(initial.Rotation());
}
constexpr Transform2d Transform2d::operator+(const Transform2d& other) const {

View File

@@ -16,7 +16,9 @@ namespace frc {
class Pose3d;
/**
* Represents a transformation for a Pose3d in the pose's frame.
* Represents a transformation for a Pose3d in the pose's frame. Translation is
* applied before rotation. (The translation is applied in the pose's original
* frame, not the transformed frame.)
*/
class WPILIB_DLLEXPORT Transform3d {
public:
@@ -192,13 +194,12 @@ class WPILIB_DLLEXPORT Transform3d {
namespace frc {
constexpr Transform3d::Transform3d(const Pose3d& initial, const Pose3d& final) {
// We are rotating the difference between the translations
// using a clockwise rotation matrix. This transforms the global
// delta into a local delta (relative to the initial pose).
// To transform the global translation delta to be relative to the initial
// pose, rotate by the inverse of the initial pose's orientation.
m_translation = (final.Translation() - initial.Translation())
.RotateBy(-initial.Rotation());
m_rotation = final.Rotation() - initial.Rotation();
m_rotation = final.Rotation().RelativeTo(initial.Rotation());
}
constexpr Transform3d Transform3d::operator+(const Transform3d& other) const {

View File

@@ -14,6 +14,8 @@
#include <wpi/SymbolExports.h>
#include "frc/geometry/Pose2d.h"
#include "frc/geometry/Pose3d.h"
#include "frc/geometry/Rotation3d.h"
#include "units/time.h"
namespace frc {
@@ -155,7 +157,8 @@ class TimeInterpolatableBuffer {
std::function<T(const T&, const T&, double)> m_interpolatingFunc;
};
// Template specialization to ensure that Pose2d uses pose exponential
// Template specializations to ensure that Pose2d and Pose3d use pose
// exponential
template <>
inline TimeInterpolatableBuffer<Pose2d>::TimeInterpolatableBuffer(
units::second_t historySize)
@@ -172,4 +175,20 @@ inline TimeInterpolatableBuffer<Pose2d>::TimeInterpolatableBuffer(
}
}) {}
template <>
inline TimeInterpolatableBuffer<Pose3d>::TimeInterpolatableBuffer(
units::second_t historySize)
: m_historySize(historySize),
m_interpolatingFunc([](const Pose3d& start, const Pose3d& end, double t) {
if (t < 0) {
return start;
} else if (t >= 1) {
return end;
} else {
Twist3d twist = start.Log(end);
Twist3d scaledTwist = twist * t;
return start.Exp(scaledTwist);
}
}) {}
} // namespace frc

View File

@@ -45,7 +45,7 @@ class WPILIB_DLLEXPORT Odometry {
m_pose(initialPose),
m_previousWheelPositions(wheelPositions) {
m_previousAngle = m_pose.Rotation();
m_gyroOffset = m_pose.Rotation() - gyroAngle;
m_gyroOffset = (-gyroAngle).RotateBy(m_pose.Rotation());
}
/**
@@ -62,7 +62,7 @@ class WPILIB_DLLEXPORT Odometry {
const WheelPositions& wheelPositions, const Pose2d& pose) {
m_pose = pose;
m_previousAngle = pose.Rotation();
m_gyroOffset = m_pose.Rotation() - gyroAngle;
m_gyroOffset = (-gyroAngle).RotateBy(m_pose.Rotation());
m_previousWheelPositions = wheelPositions;
}
@@ -72,7 +72,8 @@ class WPILIB_DLLEXPORT Odometry {
* @param pose The pose to reset to.
*/
void ResetPose(const Pose2d& pose) {
m_gyroOffset = m_gyroOffset + (pose.Rotation() - m_pose.Rotation());
m_gyroOffset =
m_gyroOffset.RotateBy(-m_pose.Rotation()).RotateBy(pose.Rotation());
m_pose = pose;
m_previousAngle = pose.Rotation();
}
@@ -92,7 +93,7 @@ class WPILIB_DLLEXPORT Odometry {
* @param rotation The rotation to reset to.
*/
void ResetRotation(const Rotation2d& rotation) {
m_gyroOffset = m_gyroOffset + (rotation - m_pose.Rotation());
m_gyroOffset = m_gyroOffset.RotateBy(m_pose.Rotation()).RotateBy(rotation);
m_pose = Pose2d{m_pose.Translation(), rotation};
m_previousAngle = rotation;
}
@@ -116,7 +117,7 @@ class WPILIB_DLLEXPORT Odometry {
*/
const Pose2d& Update(const Rotation2d& gyroAngle,
const WheelPositions& wheelPositions) {
auto angle = gyroAngle + m_gyroOffset;
auto angle = gyroAngle.RotateBy(m_gyroOffset);
auto twist =
m_kinematics.ToTwist2d(m_previousWheelPositions, wheelPositions);
@@ -136,7 +137,10 @@ class WPILIB_DLLEXPORT Odometry {
Pose2d m_pose;
WheelPositions m_previousWheelPositions;
// Always equal to m_pose.Rotation()
Rotation2d m_previousAngle;
Rotation2d m_gyroOffset;
};

View File

@@ -48,7 +48,9 @@ class WPILIB_DLLEXPORT Odometry3d {
m_pose(initialPose),
m_previousWheelPositions(wheelPositions) {
m_previousAngle = m_pose.Rotation();
m_gyroOffset = m_pose.Rotation() - gyroAngle;
// When applied extrinsically, m_gyroOffset cancels the
// current gyroAngle and then rotates to m_pose.Rotation()
m_gyroOffset = (-gyroAngle).RotateBy(m_pose.Rotation());
}
/**
@@ -65,7 +67,9 @@ class WPILIB_DLLEXPORT Odometry3d {
const WheelPositions& wheelPositions, const Pose3d& pose) {
m_pose = pose;
m_previousAngle = pose.Rotation();
m_gyroOffset = m_pose.Rotation() - gyroAngle;
// When applied extrinsically, m_gyroOffset cancels the
// current gyroAngle and then rotates to m_pose.Rotation()
m_gyroOffset = (-gyroAngle).RotateBy(m_pose.Rotation());
m_previousWheelPositions = wheelPositions;
}
@@ -75,7 +79,9 @@ class WPILIB_DLLEXPORT Odometry3d {
* @param pose The pose to reset to.
*/
void ResetPose(const Pose3d& pose) {
m_gyroOffset = m_gyroOffset + (pose.Rotation() - m_pose.Rotation());
// Cancel the previous m_pose.Rotation() and then rotate to the new angle
m_gyroOffset =
m_gyroOffset.RotateBy(-m_pose.Rotation()).RotateBy(pose.Rotation());
m_pose = pose;
m_previousAngle = pose.Rotation();
}
@@ -95,7 +101,8 @@ class WPILIB_DLLEXPORT Odometry3d {
* @param rotation The rotation to reset to.
*/
void ResetRotation(const Rotation3d& rotation) {
m_gyroOffset = m_gyroOffset + (rotation - m_pose.Rotation());
// Cancel the previous m_pose.Rotation() and then rotate to the new angle
m_gyroOffset = m_gyroOffset.RotateBy(-m_pose.Rotation()).RotateBy(rotation);
m_pose = Pose3d{m_pose.Translation(), rotation};
m_previousAngle = rotation;
}
@@ -119,7 +126,7 @@ class WPILIB_DLLEXPORT Odometry3d {
*/
const Pose3d& Update(const Rotation3d& gyroAngle,
const WheelPositions& wheelPositions) {
auto angle = gyroAngle + m_gyroOffset;
auto angle = gyroAngle.RotateBy(m_gyroOffset);
auto angle_difference = (angle - m_previousAngle).ToVector();
auto twist2d =
@@ -145,7 +152,14 @@ class WPILIB_DLLEXPORT Odometry3d {
Pose3d m_pose;
WheelPositions m_previousWheelPositions;
// Always equal to m_pose.Rotation()
Rotation3d m_previousAngle;
// Applying a rotation intrinsically to the measured gyro angle should cause
// the corrected angle to be rotated intrinsically in the same way, so the
// measured gyro angle must be applied intrinsically. This is equivalent to
// applying the offset extrinsically to the measured gyro angle.
Rotation3d m_gyroOffset;
};

View File

@@ -0,0 +1,82 @@
// 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.math.filter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.util.WPIUtilJNI;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class EdgeCounterFilterTest {
@BeforeEach
void setUp() {
WPIUtilJNI.enableMockTime();
WPIUtilJNI.setMockTime(0L);
}
@AfterEach
void tearDown() {
WPIUtilJNI.setMockTime(0L);
WPIUtilJNI.disableMockTime();
}
@Test
void edgeCounterFilterActivatedTest() {
var filter = new EdgeCounterFilter(2, 0.2);
assertFalse(filter.calculate(true)); // First edge
WPIUtilJNI.setMockTime(50_000L);
assertFalse(filter.calculate(false)); // First edge ended
WPIUtilJNI.setMockTime(100_000L);
assertTrue(filter.calculate(true)); // Second edge
WPIUtilJNI.setMockTime(150_000L);
assertTrue(filter.calculate(true)); // Still true
WPIUtilJNI.setMockTime(250_000L);
assertTrue(filter.calculate(true)); // Still true
WPIUtilJNI.setMockTime(300_000L);
assertFalse(filter.calculate(false)); // Input false, should reset
}
@Test
void edgeCounterFilterExpiredTest() {
var filter = new EdgeCounterFilter(2, 0.2);
assertFalse(filter.calculate(true)); // First edge
WPIUtilJNI.setMockTime(50_000L);
filter.calculate(false); // First edge ended
WPIUtilJNI.setMockTime(250_000L);
assertFalse(filter.calculate(true)); // Second edge after window expired
WPIUtilJNI.setMockTime(300_000L);
assertFalse(filter.calculate(true)); // Still false
}
@Test
void edgeCounterFilterParamsTest() {
var filter = new EdgeCounterFilter(2, 0.2);
assertEquals(filter.getRequiredEdges(), 2);
assertEquals(filter.getWindowTime(), 0.2);
filter.setRequiredEdges(3);
assertEquals(filter.getRequiredEdges(), 3);
filter.setWindowTime(0.5);
assertEquals(filter.getWindowTime(), 0.5);
}
}

View File

@@ -59,6 +59,16 @@ class Rotation2dTest {
assertEquals(120.0, rot.getDegrees(), kEpsilon);
}
@Test
void testRelativeTo() {
var start = Rotation2d.fromDegrees(30.0);
var end = Rotation2d.kCCW_Pi_2;
var result = end.relativeTo(start);
assertEquals(60.0, result.getDegrees(), kEpsilon);
}
@Test
void testMinus() {
var rot1 = Rotation2d.fromDegrees(70.0);

View File

@@ -281,6 +281,22 @@ class Rotation3dTest {
assertEquals(expected, rot);
}
@Test
void testRelativeTo() {
final var yAxis = VecBuilder.fill(0.0, 1.0, 0.0);
final var zAxis = VecBuilder.fill(0.0, 0.0, 1.0);
var start = new Rotation3d(yAxis, Units.degreesToRadians(-90.0));
var end = new Rotation3d(zAxis, Units.degreesToRadians(90.0));
final var intrinsicAxis = VecBuilder.fill(1.0, 1.0, 1.0);
var expected = new Rotation3d(intrinsicAxis, Units.degreesToRadians(120.0));
var result = end.relativeTo(start);
assertEquals(expected, result);
}
@Test
void testMinus() {
final var zAxis = VecBuilder.fill(0.0, 0.0, 1.0);
@@ -409,5 +425,32 @@ class Rotation3dTest {
assertEquals(Units.degreesToRadians(0.0), interpolated.getX(), kEpsilon);
assertEquals(Units.degreesToRadians(0.0), interpolated.getY(), kEpsilon);
assertEquals(Units.degreesToRadians(-175.0), interpolated.getZ(), kEpsilon);
// t value of 0 should always produce the start
rot1 = new Rotation3d(yAxis, -Units.degreesToRadians(90));
rot2 = new Rotation3d(zAxis, Units.degreesToRadians(90));
interpolated = rot1.interpolate(rot2, 0.0);
assertEquals(rot1.getX(), interpolated.getX(), kEpsilon);
assertEquals(rot1.getY(), interpolated.getY(), kEpsilon);
assertEquals(rot1.getZ(), interpolated.getZ(), kEpsilon);
// The full rotation from rot1 to rot2 to 120 degrees around extrinsic <-1.0, 1.0, 1.0>
var extrinsicAxis = VecBuilder.fill(-1.0, 1.0, 1.0);
rot1 = new Rotation3d(yAxis, -Units.degreesToRadians(90));
rot2 = new Rotation3d(zAxis, Units.degreesToRadians(90));
assertEquals(rot2, rot1.rotateBy(new Rotation3d(extrinsicAxis, Units.degreesToRadians(120))));
interpolated = rot1.interpolate(rot2, 0.5);
var expected = rot1.rotateBy(new Rotation3d(extrinsicAxis, Units.degreesToRadians(60)));
assertEquals(expected.getX(), interpolated.getX(), kEpsilon);
assertEquals(expected.getY(), interpolated.getY(), kEpsilon);
assertEquals(expected.getZ(), interpolated.getZ(), kEpsilon);
// t value of 1 should always produce the end
rot1 = new Rotation3d(yAxis, -Units.degreesToRadians(90));
rot2 = new Rotation3d(zAxis, Units.degreesToRadians(90));
interpolated = rot1.interpolate(rot2, 1.0);
assertEquals(rot2.getX(), interpolated.getX(), kEpsilon);
assertEquals(rot2.getY(), interpolated.getY(), kEpsilon);
assertEquals(rot2.getZ(), interpolated.getZ(), kEpsilon);
}
}

View File

@@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import org.junit.jupiter.api.Test;
@@ -45,4 +46,22 @@ class DifferentialDriveOdometry3dTest {
() -> assertEquals(pose.getZ(), 0.0, kEpsilon),
() -> assertEquals(pose.getRotation().toRotation2d().getDegrees(), 90.0, kEpsilon));
}
@Test
void testGyroOffset() {
m_odometry.resetPosition(
new Rotation3d(0, Units.degreesToRadians(5), 0),
0,
0,
new Pose3d(Translation3d.kZero, new Rotation3d(0, 0, Units.degreesToRadians(90))));
var pose = m_odometry.update(new Rotation3d(0, Units.degreesToRadians(10), 0), 0, 0);
assertAll(
() -> assertEquals(pose.getX(), 0.0, kEpsilon),
() -> assertEquals(pose.getY(), 0.0, kEpsilon),
() -> assertEquals(pose.getZ(), 0.0, kEpsilon),
() -> assertEquals(pose.getRotation().getX(), Units.degreesToRadians(0), kEpsilon),
() -> assertEquals(pose.getRotation().getY(), Units.degreesToRadians(5), kEpsilon),
() -> assertEquals(pose.getRotation().getZ(), Units.degreesToRadians(90), kEpsilon));
}
}

View File

@@ -295,4 +295,22 @@ class MecanumDriveOdometry3dTest {
0.05,
"Incorrect distance travelled");
}
@Test
void testGyroOffset() {
var wheelPositions = new MecanumDriveWheelPositions();
m_odometry.resetPosition(
new Rotation3d(0, Units.degreesToRadians(5), 0),
wheelPositions,
new Pose3d(Translation3d.kZero, new Rotation3d(0, 0, Units.degreesToRadians(90))));
var pose = m_odometry.update(new Rotation3d(0, Units.degreesToRadians(10), 0), wheelPositions);
assertAll(
() -> assertEquals(pose.getX(), 0.0, 1e-9),
() -> assertEquals(pose.getY(), 0.0, 1e-9),
() -> assertEquals(pose.getZ(), 0.0, 1e-9),
() -> assertEquals(pose.getRotation().getX(), Units.degreesToRadians(0), 1e-9),
() -> assertEquals(pose.getRotation().getY(), Units.degreesToRadians(5), 1e-9),
() -> assertEquals(pose.getRotation().getZ(), Units.degreesToRadians(90), 1e-9));
}
}

View File

@@ -305,4 +305,22 @@ class SwerveDriveOdometry3dTest {
0.0, errorSum / (trajectory.getTotalTimeSeconds() / dt), 0.06, "Incorrect mean error");
assertEquals(0.0, maxError, 0.125, "Incorrect max error");
}
@Test
void testGyroOffset() {
SwerveModulePosition[] modulePositions = {zero, zero, zero, zero};
m_odometry.resetPosition(
new Rotation3d(0, Units.degreesToRadians(5), 0),
modulePositions,
new Pose3d(Translation3d.kZero, new Rotation3d(0, 0, Units.degreesToRadians(90))));
var pose = m_odometry.update(new Rotation3d(0, Units.degreesToRadians(10), 0), modulePositions);
assertAll(
() -> assertEquals(pose.getX(), 0.0, 1e-9),
() -> assertEquals(pose.getY(), 0.0, 1e-9),
() -> assertEquals(pose.getZ(), 0.0, 1e-9),
() -> assertEquals(pose.getRotation().getX(), Units.degreesToRadians(0), 1e-9),
() -> assertEquals(pose.getRotation().getY(), Units.degreesToRadians(5), 1e-9),
() -> assertEquals(pose.getRotation().getZ(), Units.degreesToRadians(90), 1e-9));
}
}

View File

@@ -0,0 +1,75 @@
// 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 <gtest/gtest.h>
#include <wpi/timestamp.h>
#include "frc/filter/EdgeCountFilter.h"
#include "units/time.h"
static units::second_t now = 0_s;
class EdgeCounterFilterTest : public ::testing::Test {
protected:
void SetUp() override {
WPI_SetNowImpl([] { return units::microsecond_t{now}.to<uint64_t>(); });
now = 0_ms;
}
void TearDown() override {
now = 0_ms;
WPI_SetNowImpl(nullptr);
}
};
TEST_F(EdgeCounterFilterTest, EdgeCounterFilterActivated) {
frc::EdgeCounterFilter filter{2, 0.2_s};
EXPECT_FALSE(filter.Calculate(true)); // First edge
now = 50_ms;
EXPECT_FALSE(filter.Calculate(false)); // First edge ended
now = 100_ms;
EXPECT_TRUE(filter.Calculate(true)); // Second edge
now = 150_ms;
EXPECT_TRUE(filter.Calculate(true)); // Still true
now = 250_ms;
EXPECT_TRUE(filter.Calculate(true)); // Still true
now = 300_ms;
EXPECT_FALSE(filter.Calculate(false)); // Input false, should reset
}
TEST_F(EdgeCounterFilterTest, EdgeCounterFilterExpired) {
frc::EdgeCounterFilter filter{2, 0.2_s};
EXPECT_FALSE(filter.Calculate(true)); // First edge
now = 50_ms;
filter.Calculate(false); // First edge ended
now = 250_ms;
EXPECT_FALSE(filter.Calculate(true)); // Second edge after window expired
now = 300_ms;
EXPECT_FALSE(filter.Calculate(true)); // Still false
}
TEST_F(EdgeCounterFilterTest, EdgeCounterFilterParams) {
frc::EdgeCounterFilter filter{2, 0.2_s};
EXPECT_EQ(filter.GetRequiredEdges(), 2);
EXPECT_EQ(filter.GetWindowTime(), 0.2_s);
filter.SetRequiredEdges(3);
EXPECT_EQ(filter.GetRequiredEdges(), 3);
filter.SetWindowTime(0.5_s);
EXPECT_EQ(filter.GetWindowTime(), 0.5_s);
}

View File

@@ -42,6 +42,15 @@ TEST(Rotation2dTest, RotateByNonZero) {
EXPECT_DOUBLE_EQ(120.0, rot.Degrees().value());
}
TEST(Rotation2dTest, RelativeTo) {
auto start = Rotation2d{30_deg};
auto end = Rotation2d{90_deg};
auto result = end.RelativeTo(start);
EXPECT_DOUBLE_EQ(60.0, result.Degrees().value());
}
TEST(Rotation2dTest, Minus) {
const auto rot1 = Rotation2d{70_deg};
const auto rot2 = Rotation2d{30_deg};

View File

@@ -253,6 +253,21 @@ TEST(Rotation3dTest, RotateByNonZeroZ) {
EXPECT_EQ(expected, rot);
}
TEST(Rotation3dTest, RelativeTo) {
const Eigen::Vector3d yAxis{0.0, 1.0, 0.0};
const Eigen::Vector3d zAxis{0.0, 0.0, 1.0};
auto start = Rotation3d{yAxis, -90_deg};
auto end = Rotation3d{zAxis, 90_deg};
const Eigen::Vector3d intrinsicAxis{1.0, 1.0, 1.0};
auto expected = Rotation3d{intrinsicAxis, 120_deg};
auto result = end.RelativeTo(start);
EXPECT_EQ(expected, result);
}
TEST(Rotation3dTest, Minus) {
const Eigen::Vector3d zAxis{0.0, 0.0, 1.0};
@@ -371,4 +386,32 @@ TEST(Rotation3dTest, Interpolate) {
EXPECT_DOUBLE_EQ(0.0, units::degree_t{interpolated.X()}.value());
EXPECT_DOUBLE_EQ(0.0, units::degree_t{interpolated.Y()}.value());
EXPECT_DOUBLE_EQ(-175.0, units::degree_t{interpolated.Z()}.value());
// t value of 0 should always produce the start
rot1 = Rotation3d{yAxis, -90_deg};
rot2 = Rotation3d{zAxis, 90_deg};
interpolated = wpi::Lerp(rot1, rot2, 0.0);
EXPECT_DOUBLE_EQ(rot1.X().value(), interpolated.X().value());
EXPECT_DOUBLE_EQ(rot1.Y().value(), interpolated.Y().value());
EXPECT_DOUBLE_EQ(rot1.Z().value(), interpolated.Z().value());
// The full rotation from rot1 to rot2 is 120 degrees around extrinsic
// <-1.0, 1.0, 1.0>
const Eigen::Vector3d extrinsicAxis{-1.0, 1.0, 1.0};
rot1 = Rotation3d{yAxis, -90_deg};
rot2 = Rotation3d{zAxis, 90_deg};
EXPECT_EQ(rot2, rot1.RotateBy(Rotation3d{extrinsicAxis, 120_deg}));
interpolated = wpi::Lerp(rot1, rot2, 0.5);
auto expected = rot1.RotateBy(Rotation3d{extrinsicAxis, 60_deg});
EXPECT_DOUBLE_EQ(expected.X().value(), interpolated.X().value());
EXPECT_DOUBLE_EQ(expected.Y().value(), interpolated.Y().value());
EXPECT_DOUBLE_EQ(expected.Z().value(), interpolated.Z().value());
// t value of 1 should always produce the end
rot1 = Rotation3d{yAxis, -90_deg};
rot2 = Rotation3d{zAxis, 90_deg};
interpolated = wpi::Lerp(rot1, rot2, 1.0);
EXPECT_NEAR(rot2.X().value(), interpolated.X().value(), 1e-9);
EXPECT_NEAR(rot2.Y().value(), interpolated.Y().value(), 1e-9);
EXPECT_NEAR(rot2.Z().value(), interpolated.Z().value(), 1e-9);
}

View File

@@ -38,3 +38,18 @@ TEST(DifferentialDriveOdometry3dTest, EncoderDistances) {
EXPECT_NEAR(pose.Z().value(), 0.0, kEpsilon);
EXPECT_NEAR(pose.Rotation().ToRotation2d().Degrees().value(), 90.0, kEpsilon);
}
TEST(DifferentialDriveOdometry3dTest, GyroOffset) {
DifferentialDriveOdometry3d odometry{
frc::Rotation3d{0_deg, 5_deg, 0_deg}, 0_m, 0_m,
frc::Pose3d{frc::Translation3d{}, frc::Rotation3d{0_deg, 0_deg, 90_deg}}};
const auto& pose =
odometry.Update(frc::Rotation3d{0_deg, 10_deg, 0_deg}, 0_m, 0_m);
EXPECT_NEAR(pose.X().value(), 0.0, kEpsilon);
EXPECT_NEAR(pose.Y().value(), 0.0, kEpsilon);
EXPECT_NEAR(pose.Z().value(), 0.0, kEpsilon);
EXPECT_NEAR(units::degree_t{pose.Rotation().X()}.value(), 0.0, kEpsilon);
EXPECT_NEAR(units::degree_t{pose.Rotation().Y()}.value(), 5.0, kEpsilon);
EXPECT_NEAR(units::degree_t{pose.Rotation().Z()}.value(), 90.0, kEpsilon);
}

View File

@@ -221,3 +221,19 @@ TEST_F(MecanumDriveOdometry3dTest, AccuracyFacingXAxis) {
EXPECT_LT(errorSum / (trajectory.TotalTime().value() / dt.value()), 0.06);
EXPECT_LT(maxError, 0.125);
}
TEST_F(MecanumDriveOdometry3dTest, GyroOffset) {
frc::MecanumDriveWheelPositions wheelPositions;
odometry.ResetPosition(
frc::Rotation3d{0_deg, 5_deg, 0_deg}, wheelPositions,
frc::Pose3d{frc::Translation3d{}, frc::Rotation3d{0_deg, 0_deg, 90_deg}});
auto pose =
odometry.Update(frc::Rotation3d{0_deg, 10_deg, 0_deg}, wheelPositions);
EXPECT_NEAR(pose.X().value(), 0.0, 1e-9);
EXPECT_NEAR(pose.Y().value(), 0.0, 1e-9);
EXPECT_NEAR(pose.Z().value(), 0.0, 1e-9);
EXPECT_NEAR(units::degree_t{pose.Rotation().X()}.value(), 0.0, 1e-9);
EXPECT_NEAR(units::degree_t{pose.Rotation().Y()}.value(), 5.0, 1e-9);
EXPECT_NEAR(units::degree_t{pose.Rotation().Z()}.value(), 90.0, 1e-9);
}

View File

@@ -222,3 +222,18 @@ TEST_F(SwerveDriveOdometry3dTest, AccuracyFacingXAxis) {
EXPECT_LT(errorSum / (trajectory.TotalTime().value() / dt.value()), 0.06);
EXPECT_LT(maxError, 0.125);
}
TEST_F(SwerveDriveOdometry3dTest, GyroOffset) {
m_odometry.ResetPosition(
frc::Rotation3d{0_deg, 5_deg, 0_deg}, {zero, zero, zero, zero},
frc::Pose3d{frc::Translation3d{}, frc::Rotation3d{0_deg, 0_deg, 90_deg}});
auto pose = m_odometry.Update(frc::Rotation3d{0_deg, 10_deg, 0_deg},
{zero, zero, zero, zero});
EXPECT_NEAR(pose.X().value(), 0.0, 1e-9);
EXPECT_NEAR(pose.Y().value(), 0.0, 1e-9);
EXPECT_NEAR(pose.Z().value(), 0.0, 1e-9);
EXPECT_NEAR(units::degree_t{pose.Rotation().X()}.value(), 0.0, 1e-9);
EXPECT_NEAR(units::degree_t{pose.Rotation().Y()}.value(), 5.0, 1e-9);
EXPECT_NEAR(units::degree_t{pose.Rotation().Z()}.value(), 90.0, 1e-9);
}