diff --git a/.github/workflows/cmake-android.yml b/.github/workflows/cmake-android.yml index 90ec646e48..dbaf7c5fc9 100644 --- a/.github/workflows/cmake-android.yml +++ b/.github/workflows/cmake-android.yml @@ -30,7 +30,7 @@ jobs: - uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: r27c + ndk-version: r27d add-to-path: false - uses: actions/setup-java@v5 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 97bb28ec00..ba8b64ec7c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -56,8 +56,7 @@ jobs: run: echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV if: startsWith(github.ref, 'refs/tags/v2027') - name: Build with Gradle - # main on 2021-05-30, to include addnab/docker-run-action/pull/17 - 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 diff --git a/.github/workflows/sentinel-build.yml b/.github/workflows/sentinel-build.yml index dd6c3d359b..1bf91a1792 100644 --- a/.github/workflows/sentinel-build.yml +++ b/.github/workflows/sentinel-build.yml @@ -50,8 +50,7 @@ jobs: with: fetch-depth: 0 - name: Build with Gradle - # main on 2021-05-30, to include addnab/docker-run-action/pull/17 - 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index d2408b0c36..2943dc85ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -349,7 +349,9 @@ if(WITH_WPILIB) set(APRILTAG_DEP_REPLACE "find_dependency(apriltag)") set(COMMANDSV3_DEP_REPLACE "find_dependency(commandsv3)") set(WPILIBC_DEP_REPLACE "find_dependency(wpilibc)") - set(WPILIBJ_DEP_REPLACE "find_dependency(wpilibj)") + if(WITH_JAVA) + set(WPILIBJ_DEP_REPLACE "find_dependency(wpilibj)") + endif() set(COMMAND_DEP_REPLACE "find_dependency(commandsv2)") add_subdirectory(apriltag) add_subdirectory(wpilibj) diff --git a/LICENSE.md b/LICENSE.md index d744196fe9..eb3061b0d8 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2009-2025 FIRST and other WPILib contributors +Copyright (c) 2009-2026 FIRST and other WPILib contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/apriltag/src/main/java/org/wpilib/vision/apriltag/AprilTagFields.java b/apriltag/src/main/java/org/wpilib/vision/apriltag/AprilTagFields.java index d2fb0199cc..7b3641029c 100644 --- a/apriltag/src/main/java/org/wpilib/vision/apriltag/AprilTagFields.java +++ b/apriltag/src/main/java/org/wpilib/vision/apriltag/AprilTagFields.java @@ -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 = "/org/wpilib/vision/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; diff --git a/apriltag/src/main/native/cpp/AprilTagFieldLayout.cpp b/apriltag/src/main/native/cpp/AprilTagFieldLayout.cpp index 255321198a..b7830edcf8 100644 --- a/apriltag/src/main/native/cpp/AprilTagFieldLayout.cpp +++ b/apriltag/src/main/native/cpp/AprilTagFieldLayout.cpp @@ -141,6 +141,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 wpi::apriltag @@ -162,6 +164,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"); } diff --git a/apriltag/src/main/native/include/wpi/apriltag/AprilTagFields.hpp b/apriltag/src/main/native/include/wpi/apriltag/AprilTagFields.hpp index 4d73d41de0..2647130cef 100644 --- a/apriltag/src/main/native/include/wpi/apriltag/AprilTagFields.hpp +++ b/apriltag/src/main/native/include/wpi/apriltag/AprilTagFields.hpp @@ -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 diff --git a/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-andymark.csv b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-andymark.csv new file mode 100644 index 0000000000..da80d5fa95 --- /dev/null +++ b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-andymark.csv @@ -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 diff --git a/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-andymark.json b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-andymark.json new file mode 100644 index 0000000000..ecc03907da --- /dev/null +++ b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-andymark.json @@ -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 + } +} diff --git a/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-welded.csv b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-welded.csv new file mode 100644 index 0000000000..39f20f6bae --- /dev/null +++ b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-welded.csv @@ -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 diff --git a/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-welded.json b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-welded.json new file mode 100644 index 0000000000..8c5a52de87 --- /dev/null +++ b/apriltag/src/main/native/resources/org/wpilib/vision/apriltag/2026-rebuilt-welded.json @@ -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 + } +} diff --git a/commandsv2/src/main/java/org/wpilib/command2/button/Trigger.java b/commandsv2/src/main/java/org/wpilib/command2/button/Trigger.java index a2e98d41a1..410a90ed61 100644 --- a/commandsv2/src/main/java/org/wpilib/command2/button/Trigger.java +++ b/commandsv2/src/main/java/org/wpilib/command2/button/Trigger.java @@ -11,6 +11,7 @@ import org.wpilib.command2.Command; import org.wpilib.command2.CommandScheduler; import org.wpilib.event.EventLoop; import org.wpilib.math.filter.Debouncer; +import org.wpilib.math.filter.EdgeCounterFilter; /** * This class provides an easy way to link commands to conditions. @@ -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. + * + *

This is useful for implementing "double-click" style functionality. + * + *

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()); + } + }); + } } diff --git a/commandsv2/src/main/native/cpp/frc2/command/button/Trigger.cpp b/commandsv2/src/main/native/cpp/frc2/command/button/Trigger.cpp index 8382d40f75..cb36990849 100644 --- a/commandsv2/src/main/native/cpp/frc2/command/button/Trigger.cpp +++ b/commandsv2/src/main/native/cpp/frc2/command/button/Trigger.cpp @@ -8,6 +8,7 @@ #include "wpi/commands2/CommandPtr.hpp" #include "wpi/math/filter/Debouncer.hpp" +#include "wpi/math/filter/EdgeCounterFilter.hpp" using namespace wpi; using namespace wpi::cmd; @@ -183,6 +184,14 @@ Trigger Trigger::Debounce(wpi::units::second_t debounceTime, }); } +Trigger Trigger::MultiPress(int requiredPresses, units::second_t windowTime) { + return Trigger(m_loop, [filter = wpi::math::EdgeCounterFilter(requiredPresses, + windowTime), + condition = m_condition]() mutable { + return filter.Calculate(condition()); + }); +} + bool Trigger::Get() const { return m_condition(); } diff --git a/commandsv2/src/main/native/include/wpi/commands2/button/Trigger.hpp b/commandsv2/src/main/native/include/wpi/commands2/button/Trigger.hpp index ac3ffe3bbf..c7b923962e 100644 --- a/commandsv2/src/main/native/include/wpi/commands2/button/Trigger.hpp +++ b/commandsv2/src/main/native/include/wpi/commands2/button/Trigger.hpp @@ -283,6 +283,22 @@ class Trigger { wpi::math::Debouncer::DebounceType type = wpi::math::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. + * + *

This is useful for implementing "double-click" style functionality. + * + *

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. * diff --git a/cscore/src/main/native/objcpp/UvcControlImpl.mm b/cscore/src/main/native/objcpp/UvcControlImpl.mm index 88699b0a83..c68b1797ec 100644 --- a/cscore/src/main/native/objcpp/UvcControlImpl.mm +++ b/cscore/src/main/native/objcpp/UvcControlImpl.mm @@ -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; } diff --git a/fields/src/main/java/org/wpilib/fields/Fields.java b/fields/src/main/java/org/wpilib/fields/Fields.java index 1bd1532c8f..df804dff67 100644 --- a/fields/src/main/java/org/wpilib/fields/Fields.java +++ b/fields/src/main/java/org/wpilib/fields/Fields.java @@ -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 = "/org/wpilib/fields/"; /** Alias to the current game. */ - public static final Fields kDefaultField = k2025Reefscape; + public static final Fields kDefaultField = k2026Rebuilt; public final String m_resourceFile; diff --git a/fields/src/main/native/cpp/fields.cpp b/fields/src/main/native/cpp/fields.cpp index 84f91cc468..f3606ccf1c 100644 --- a/fields/src/main/native/cpp/fields.cpp +++ b/fields/src/main/native/cpp/fields.cpp @@ -17,10 +17,12 @@ #include "wpi/fields/2023-chargedup.hpp" #include "wpi/fields/2024-crescendo.hpp" #include "wpi/fields/2025-reefscape.hpp" +#include "wpi/fields/2026-rebuilt.hpp" using namespace wpi::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, diff --git a/fields/src/main/native/include/wpi/fields/2026-rebuilt.hpp b/fields/src/main/native/include/wpi/fields/2026-rebuilt.hpp new file mode 100644 index 0000000000..25a3ad1f45 --- /dev/null +++ b/fields/src/main/native/include/wpi/fields/2026-rebuilt.hpp @@ -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 + +namespace wpi::fields { +std::string_view GetResource_2026_rebuilt_json(); +std::string_view GetResource_2026_field_png(); +} // namespace wpi::fields diff --git a/fields/src/main/native/resources/org/wpilib/fields/2026-field.png b/fields/src/main/native/resources/org/wpilib/fields/2026-field.png new file mode 100644 index 0000000000..b4f0295e8e Binary files /dev/null and b/fields/src/main/native/resources/org/wpilib/fields/2026-field.png differ diff --git a/fields/src/main/native/resources/org/wpilib/fields/2026-rebuilt.json b/fields/src/main/native/resources/org/wpilib/fields/2026-rebuilt.json new file mode 100644 index 0000000000..bc8675c0d4 --- /dev/null +++ b/fields/src/main/native/resources/org/wpilib/fields/2026-rebuilt.json @@ -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" +} diff --git a/glass/src/lib/native/cpp/other/FMS.cpp b/glass/src/lib/native/cpp/other/FMS.cpp index 1fd6ef5701..965c6e1bb6 100644 --- a/glass/src/lib/native/cpp/other/FMS.cpp +++ b/glass/src/lib/native/cpp/other/FMS.cpp @@ -73,11 +73,11 @@ void wpi::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); } } diff --git a/glass/src/lib/native/cpp/other/Field2D.cpp b/glass/src/lib/native/cpp/other/Field2D.cpp index 099647102c..87cc5dc538 100644 --- a/glass/src/lib/native/cpp/other/Field2D.cpp +++ b/glass/src/lib/native/cpp/other/Field2D.cpp @@ -348,7 +348,7 @@ static bool InputPose(wpi::math::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())}, m_height{storage.GetFloat("height", kDefaultHeight.to())}, diff --git a/hal/src/main/java/org/wpilib/hardware/hal/DriverStationJNI.java b/hal/src/main/java/org/wpilib/hardware/hal/DriverStationJNI.java index a91ab76f3f..81fab9161b 100644 --- a/hal/src/main/java/org/wpilib/hardware/hal/DriverStationJNI.java +++ b/hal/src/main/java/org/wpilib/hardware/hal/DriverStationJNI.java @@ -241,16 +241,19 @@ public class DriverStationJNI extends JNIWrapper { public static native String getJoystickName(byte joystickNum); /** - * 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). * - *

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). + *

When connected to the real field, this number only changes in full integer increments, and + * always counts down. * - *

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). + *

When the DS is in practice mode, this number is a floating point number, and counts down. * - *

The Practice Match function of the DS approximates the behavior seen on the field. + *

When the DS is in teleop or autonomous mode, this number returns -1.0. + * + *

Simulation matches DS behavior without an FMS connected. * * @return time remaining in current match period (auto or teleop) * @see "HAL_GetMatchTime" diff --git a/hal/src/main/native/include/wpi/hal/DriverStation.h b/hal/src/main/native/include/wpi/hal/DriverStation.h index 02c3ec0102..a244ee1c6d 100644 --- a/hal/src/main/native/include/wpi/hal/DriverStation.h +++ b/hal/src/main/native/include/wpi/hal/DriverStation.h @@ -240,8 +240,7 @@ int32_t HAL_SetJoystickLeds(int32_t joystickNum, int32_t leds); *

When the DS is in practice mode, this number is a floating point number, * and counts down. * - *

When the DS is in teleop or autonomous mode, this number is a floating - * point number, and counts up. + *

When the DS is in teleop or autonomous mode, this number returns -1.0. * *

Simulation matches DS behavior without an FMS connected. * diff --git a/tools/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp b/tools/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp index 9e5b1d1b04..8e57451694 100644 --- a/tools/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp +++ b/tools/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp @@ -57,7 +57,8 @@ static double Lerp(wpi::units::second_t time, */ static std::vector ConvertToPrepared(const MotorData& data) { std::vector 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(run.voltage.size()) - 1; ++i) { @@ -101,7 +102,7 @@ static void CopyRawData(wpi::util::StringMap* 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()); } diff --git a/wpilibc/src/main/native/cpp/hardware/led/LEDPattern.cpp b/wpilibc/src/main/native/cpp/hardware/led/LEDPattern.cpp index e738ba3d43..05bd5c698e 100644 --- a/wpilibc/src/main/native/cpp/hardware/led/LEDPattern.cpp +++ b/wpilibc/src/main/native/cpp/hardware/led/LEDPattern.cpp @@ -11,6 +11,7 @@ #include #include +#include "wpi/hal/UsageReporting.h" #include "wpi/math/util/MathUtil.hpp" #include "wpi/util/MathExtras.hpp" #include "wpi/util/timestamp.h" @@ -21,7 +22,9 @@ LEDPattern::LEDPattern( std::function)> impl) - : m_impl(std::move(impl)) {} + : m_impl(std::move(impl)) { + HAL_ReportUsage("LEDPattern", ""); +} void LEDPattern::ApplyTo( LEDPattern::LEDReader reader, diff --git a/wpilibc/src/main/native/include/wpi/drive/DifferentialDrive.hpp b/wpilibc/src/main/native/include/wpi/drive/DifferentialDrive.hpp index 4446698dd4..ebc80f866d 100644 --- a/wpilibc/src/main/native/include/wpi/drive/DifferentialDrive.hpp +++ b/wpilibc/src/main/native/include/wpi/drive/DifferentialDrive.hpp @@ -76,7 +76,7 @@ class DifferentialDrive : public RobotDriveBase, * Construct a DifferentialDrive. * * To pass multiple motors per side, use CAN motor controller followers or - * PWMSpeedController::AddFollower(). If a motor needs to be inverted, do so + * PWMMotorController::AddFollower(). If a motor needs to be inverted, do so * before passing it in. * * @param leftMotor Left motor. @@ -90,7 +90,7 @@ class DifferentialDrive : public RobotDriveBase, * Construct a DifferentialDrive. * * To pass multiple motors per side, use CAN motor controller followers or - * PWMSpeedController::AddFollower(). If a motor needs to be inverted, do so + * PWMMotorController::AddFollower(). If a motor needs to be inverted, do so * before passing it in. * * @param leftMotor Left motor setter. diff --git a/wpilibc/src/main/native/include/wpi/driverstation/DriverStation.hpp b/wpilibc/src/main/native/include/wpi/driverstation/DriverStation.hpp index 7676dd07b8..309e7a3283 100644 --- a/wpilibc/src/main/native/include/wpi/driverstation/DriverStation.hpp +++ b/wpilibc/src/main/native/include/wpi/driverstation/DriverStation.hpp @@ -587,8 +587,7 @@ class DriverStation final { *

When the DS is in practice mode, this number is a floating point number, * and counts down. * - *

When the DS is in teleop or autonomous mode, this number is a floating - * point number, and counts up. + *

When the DS is in teleop or autonomous mode, this number returns -1.0. * *

Simulation matches DS behavior without an FMS connected. * diff --git a/wpilibc/src/main/native/include/wpi/framework/TimedRobot.hpp b/wpilibc/src/main/native/include/wpi/framework/TimedRobot.hpp index facc6e2025..55dca9c5d2 100644 --- a/wpilibc/src/main/native/include/wpi/framework/TimedRobot.hpp +++ b/wpilibc/src/main/native/include/wpi/framework/TimedRobot.hpp @@ -64,7 +64,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). diff --git a/wpilibc/src/main/native/include/wpi/system/Timer.hpp b/wpilibc/src/main/native/include/wpi/system/Timer.hpp index 03820002e1..e9264ddcc6 100644 --- a/wpilibc/src/main/native/include/wpi/system/Timer.hpp +++ b/wpilibc/src/main/native/include/wpi/system/Timer.hpp @@ -139,19 +139,23 @@ class Timer { static wpi::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. + *

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) + *

When the DS is in practice mode, this number is a floating point number, + * and counts down. + * + *

When the DS is in teleop or autonomous mode, this number returns -1.0. + * + *

Simulation matches DS behavior without an FMS connected. + * + * @return Time remaining in current match period (auto or teleop) in seconds */ static wpi::units::second_t GetMatchTime(); diff --git a/wpilibcExamples/src/main/cpp/examples/Xrptimed/cpp/Robot.cpp b/wpilibcExamples/src/main/cpp/examples/Xrptimed/cpp/Robot.cpp new file mode 100644 index 0000000000..5ea68bdcc2 --- /dev/null +++ b/wpilibcExamples/src/main/cpp/examples/Xrptimed/cpp/Robot.cpp @@ -0,0 +1,56 @@ +// 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.hpp" + +Robot::Robot() { + wpi::util::SendableRegistry::AddChild(&m_drive, &m_leftMotor); + wpi::util::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. + * + *

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_controller.GetRawAxis(2), -m_controller.GetRawAxis(1)); +} + +#ifndef RUNNING_FRC_TESTS +int main() { + return wpi::StartRobot(); +} +#endif diff --git a/wpilibcExamples/src/main/cpp/examples/Xrptimed/include/Robot.hpp b/wpilibcExamples/src/main/cpp/examples/Xrptimed/include/Robot.hpp new file mode 100644 index 0000000000..b3d4e896d0 --- /dev/null +++ b/wpilibcExamples/src/main/cpp/examples/Xrptimed/include/Robot.hpp @@ -0,0 +1,32 @@ +// 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/drive/DifferentialDrive.hpp" +#include "wpi/driverstation/Joystick.hpp" +#include "wpi/framework/TimedRobot.hpp" +#include "wpi/system/Timer.hpp" +#include "wpi/xrp/XRPMotor.hpp" + +class Robot : public wpi::TimedRobot { + public: + Robot(); + void RobotPeriodic() override; + void AutonomousInit() override; + void AutonomousPeriodic() override; + void TeleopInit() override; + void TeleopPeriodic() override; + + private: + wpi::xrp::XRPMotor m_leftMotor{0}; + wpi::xrp::XRPMotor m_rightMotor{1}; + // Assumes a gamepad plugged into channel 0 + wpi::Joystick m_controller{0}; + wpi::Timer m_timer; + + wpi::DifferentialDrive m_drive{ + [&](double output) { m_leftMotor.Set(output); }, + [&](double output) { m_rightMotor.Set(output); }}; +}; diff --git a/wpilibcExamples/src/main/cpp/examples/examples.json b/wpilibcExamples/src/main/cpp/examples/examples.json index fd49884765..13ddf8df49 100644 --- a/wpilibcExamples/src/main/cpp/examples/examples.json +++ b/wpilibcExamples/src/main/cpp/examples/examples.json @@ -510,6 +510,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", + "Joystick" + ], + "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.", diff --git a/wpilibj/src/main/java/org/wpilib/driverstation/DriverStation.java b/wpilibj/src/main/java/org/wpilib/driverstation/DriverStation.java index 9ef82723ca..962257b9d0 100644 --- a/wpilibj/src/main/java/org/wpilib/driverstation/DriverStation.java +++ b/wpilibj/src/main/java/org/wpilib/driverstation/DriverStation.java @@ -1733,8 +1733,7 @@ public final class DriverStation { * *

When the DS is in practice mode, this number is a floating point number, and counts down. * - *

When the DS is in teleop or autonomous mode, this number is a floating point number, and - * counts up. + *

When the DS is in teleop or autonomous mode, this number returns -1.0. * *

Simulation matches DS behavior without an FMS connected. * diff --git a/wpilibj/src/main/java/org/wpilib/framework/TimedRobot.java b/wpilibj/src/main/java/org/wpilib/framework/TimedRobot.java index dc6a75c3ab..bd4205f4b5 100644 --- a/wpilibj/src/main/java/org/wpilib/framework/TimedRobot.java +++ b/wpilibj/src/main/java/org/wpilib/framework/TimedRobot.java @@ -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. */ diff --git a/wpilibj/src/main/java/org/wpilib/hardware/led/LEDPattern.java b/wpilibj/src/main/java/org/wpilib/hardware/led/LEDPattern.java index 51de594e64..a43204fabb 100644 --- a/wpilibj/src/main/java/org/wpilib/hardware/led/LEDPattern.java +++ b/wpilibj/src/main/java/org/wpilib/hardware/led/LEDPattern.java @@ -14,6 +14,7 @@ import java.util.Objects; import java.util.function.BooleanSupplier; import java.util.function.DoubleSupplier; import org.wpilib.driverstation.DriverStation; +import org.wpilib.hardware.hal.HAL; import org.wpilib.system.RobotController; import org.wpilib.units.collections.LongToObjectHashMap; import org.wpilib.units.measure.Dimensionless; @@ -150,6 +151,7 @@ public interface LEDPattern { * @return the mapped pattern */ default LEDPattern mapIndex(IndexMapper indexMapper) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { int bufLen = reader.getLength(); applyTo( @@ -295,6 +297,7 @@ public interface LEDPattern { final long totalTimeMicros = (long) (onTime.in(Microseconds) + offTime.in(Microseconds)); final long onTimeMicros = (long) onTime.in(Microseconds); + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { if (RobotController.getTime() % totalTimeMicros < onTimeMicros) { applyTo(reader, writer); @@ -324,6 +327,7 @@ public interface LEDPattern { * @return the blinking pattern */ default LEDPattern synchronizedBlink(BooleanSupplier signal) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { if (signal.getAsBoolean()) { applyTo(reader, writer); @@ -343,6 +347,7 @@ public interface LEDPattern { default LEDPattern breathe(Time period) { final long periodMicros = (long) period.in(Microseconds); + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { applyTo( reader, @@ -374,6 +379,7 @@ public interface LEDPattern { * @return the combined overlay pattern */ default LEDPattern overlayOn(LEDPattern base) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { // write the base pattern down first... base.applyTo(reader, writer); @@ -401,6 +407,7 @@ public interface LEDPattern { * @return the blended pattern */ default LEDPattern blend(LEDPattern other) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { applyTo(reader, writer); @@ -432,6 +439,7 @@ public interface LEDPattern { * @return the masked pattern */ default LEDPattern mask(LEDPattern mask) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { // Apply the current pattern down as normal... applyTo(reader, writer); @@ -471,6 +479,7 @@ public interface LEDPattern { default LEDPattern atBrightness(Dimensionless relativeBrightness) { double multiplier = relativeBrightness.in(Value); + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { applyTo( reader, @@ -497,6 +506,7 @@ public interface LEDPattern { * @return the pattern */ static LEDPattern solid(Color color) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { int bufLen = reader.getLength(); for (int led = 0; led < bufLen; led++) { @@ -526,6 +536,7 @@ public interface LEDPattern { * @return the mask pattern */ static LEDPattern progressMaskLayer(DoubleSupplier progressSupplier) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { double progress = Math.clamp(progressSupplier.getAsDouble(), 0, 1); @@ -562,6 +573,7 @@ public interface LEDPattern { * @return a motionless step pattern */ static LEDPattern steps(Map steps) { + HAL.reportUsage("LEDPattern", ""); if (steps.isEmpty()) { // no colors specified DriverStation.reportWarning("Creating LED steps with no colors!", false); @@ -623,6 +635,7 @@ public interface LEDPattern { * @return a motionless gradient pattern */ static LEDPattern gradient(GradientType type, Color... colors) { + HAL.reportUsage("LEDPattern", ""); if (colors.length == 0) { // Nothing to display DriverStation.reportWarning("Creating a gradient with no colors!", false); @@ -680,6 +693,7 @@ public interface LEDPattern { * @return the rainbow pattern */ static LEDPattern rainbow(int saturation, int value) { + HAL.reportUsage("LEDPattern", ""); return (reader, writer) -> { int bufLen = reader.getLength(); for (int i = 0; i < bufLen; i++) { diff --git a/wpilibj/src/main/java/org/wpilib/system/Timer.java b/wpilibj/src/main/java/org/wpilib/system/Timer.java index 790db4b94d..5620d24da7 100644 --- a/wpilibj/src/main/java/org/wpilib/system/Timer.java +++ b/wpilibj/src/main/java/org/wpilib/system/Timer.java @@ -41,8 +41,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). + * + *

When connected to the real field, this number only changes in full integer increments, and + * always counts down. + * + *

When the DS is in practice mode, this number is a floating point number, and counts down. + * + *

When the DS is in teleop or autonomous mode, this number returns -1.0. + * + *

Simulation matches DS behavior without an FMS connected. * * @return Time remaining in current match period (auto or teleop) in seconds */ diff --git a/wpilibjExamples/src/main/java/org/wpilib/examples/examples.json b/wpilibjExamples/src/main/java/org/wpilib/examples/examples.json index 765abb2424..16e95134c9 100644 --- a/wpilibjExamples/src/main/java/org/wpilib/examples/examples.json +++ b/wpilibjExamples/src/main/java/org/wpilib/examples/examples.json @@ -706,6 +706,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", + "Joystick" + ], + "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.", diff --git a/wpilibjExamples/src/main/java/org/wpilib/examples/xrptimed/Main.java b/wpilibjExamples/src/main/java/org/wpilib/examples/xrptimed/Main.java new file mode 100644 index 0000000000..718bd8b36c --- /dev/null +++ b/wpilibjExamples/src/main/java/org/wpilib/examples/xrptimed/Main.java @@ -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 org.wpilib.examples.xrptimed; + +import org.wpilib.framework.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. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/wpilibjExamples/src/main/java/org/wpilib/examples/xrptimed/Robot.java b/wpilibjExamples/src/main/java/org/wpilib/examples/xrptimed/Robot.java new file mode 100644 index 0000000000..1abcf0e8eb --- /dev/null +++ b/wpilibjExamples/src/main/java/org/wpilib/examples/xrptimed/Robot.java @@ -0,0 +1,74 @@ +// 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 org.wpilib.examples.xrptimed; + +import org.wpilib.drive.DifferentialDrive; +import org.wpilib.driverstation.Joystick; +import org.wpilib.framework.TimedRobot; +import org.wpilib.system.Timer; +import org.wpilib.util.sendable.SendableRegistry; +import org.wpilib.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); + // Assumes a gamepad plugged into channel 0 + private final Joystick m_controller = new Joystick(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.getRawAxis(2), -m_controller.getRawAxis(1)); + } + + /** 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() {} +} diff --git a/wpimath/src/main/java/org/wpilib/math/controller/LinearQuadraticRegulator.java b/wpimath/src/main/java/org/wpilib/math/controller/LinearQuadraticRegulator.java index bc43dba74f..fbd62fecef 100644 --- a/wpimath/src/main/java/org/wpilib/math/controller/LinearQuadraticRegulator.java +++ b/wpimath/src/main/java/org/wpilib/math/controller/LinearQuadraticRegulator.java @@ -11,6 +11,7 @@ import org.wpilib.math.linalg.Vector; import org.wpilib.math.numbers.N1; import org.wpilib.math.system.Discretization; import org.wpilib.math.system.LinearSystem; +import org.wpilib.math.util.MathSharedStore; import org.wpilib.math.util.Num; import org.wpilib.math.util.StateSpaceUtil; @@ -119,6 +120,7 @@ public class LinearQuadraticRegulator(new SimpleMatrix(B.getNumCols(), 1)); reset(); + MathSharedStore.getMathShared().reportUsage("LinearQuadraticRegulator", ""); } /** @@ -158,6 +160,7 @@ public class LinearQuadraticRegulator(new SimpleMatrix(B.getNumCols(), 1)); reset(); + MathSharedStore.getMathShared().reportUsage("LinearQuadraticRegulator", ""); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/estimator/ExtendedKalmanFilter.java b/wpimath/src/main/java/org/wpilib/math/estimator/ExtendedKalmanFilter.java index 63bad3206f..670fd27b21 100644 --- a/wpimath/src/main/java/org/wpilib/math/estimator/ExtendedKalmanFilter.java +++ b/wpimath/src/main/java/org/wpilib/math/estimator/ExtendedKalmanFilter.java @@ -12,6 +12,7 @@ import org.wpilib.math.system.Discretization; import org.wpilib.math.system.LinearSystemUtil; import org.wpilib.math.system.NumericalIntegration; import org.wpilib.math.system.NumericalJacobian; +import org.wpilib.math.util.MathSharedStore; import org.wpilib.math.util.Nat; import org.wpilib.math.util.Num; import org.wpilib.math.util.StateSpaceUtil; @@ -164,6 +165,8 @@ public class ExtendedKalmanFilter(DARE.dare(discA.transpose(), C.transpose(), discQ, discR)); reset(); + + MathSharedStore.getMathShared().reportUsage("KalmanFilter", ""); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator.java b/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator.java index 0e86941138..de8d941630 100644 --- a/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator.java +++ b/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator.java @@ -15,11 +15,9 @@ import org.wpilib.math.interpolation.TimeInterpolatableBuffer; import org.wpilib.math.kinematics.Kinematics; import org.wpilib.math.kinematics.Odometry; import org.wpilib.math.linalg.Matrix; -import org.wpilib.math.linalg.VecBuilder; import org.wpilib.math.numbers.N1; import org.wpilib.math.numbers.N3; import org.wpilib.math.util.MathSharedStore; -import org.wpilib.math.util.Nat; /** * This class wraps {@link Odometry} to fuse latency-compensated vision measurements with encoder @@ -38,8 +36,12 @@ import org.wpilib.math.util.Nat; */ public class PoseEstimator { private final Odometry m_odometry; - private final Matrix m_q = new Matrix<>(Nat.N3(), Nat.N1()); - private final Matrix 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 @@ -47,7 +49,9 @@ public class PoseEstimator { TimeInterpolatableBuffer.createBuffer(kBufferDuration); // Maps timestamps to vision updates // Always contains one entry before the oldest entry in m_odometryPoseBuffer, unless there have - // been no vision measurements after the last reset + // been no vision measurements after the last reset. May contain one entry while + // m_odometryPoseBuffer is empty to correct for translation/rotation after a call to + // ResetRotation/ResetTranslation. private final NavigableMap m_visionUpdates = new TreeMap<>(); private Pose2d m_poseEstimate; @@ -75,9 +79,10 @@ public class PoseEstimator { m_poseEstimate = m_odometry.getPose(); 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("PoseEstimator", ""); } /** @@ -90,6 +95,7 @@ public class PoseEstimator { * theta]ᵀ, with units in meters and radians. */ public final void setVisionMeasurementStdDevs(Matrix 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); @@ -98,11 +104,10 @@ public class PoseEstimator { // 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])); } } } @@ -144,9 +149,22 @@ public class PoseEstimator { */ public void resetTranslation(Translation2d translation) { m_odometry.resetTranslation(translation); + + final var latestVisionUpdate = m_visionUpdates.lastEntry(); m_odometryPoseBuffer.clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.getPose(); + + if (latestVisionUpdate != null) { + // apply vision compensation to the pose rotation + final var visionUpdate = + new VisionUpdate( + new Pose2d(translation, latestVisionUpdate.getValue().visionPose.getRotation()), + new Pose2d(translation, latestVisionUpdate.getValue().odometryPose.getRotation())); + m_visionUpdates.put(latestVisionUpdate.getKey(), visionUpdate); + m_poseEstimate = visionUpdate.compensate(m_odometry.getPose()); + } else { + m_poseEstimate = m_odometry.getPose(); + } } /** @@ -156,9 +174,22 @@ public class PoseEstimator { */ public void resetRotation(Rotation2d rotation) { m_odometry.resetRotation(rotation); + + final var latestVisionUpdate = m_visionUpdates.lastEntry(); m_odometryPoseBuffer.clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.getPose(); + + if (latestVisionUpdate != null) { + // apply vision compensation to the pose translation + final var visionUpdate = + new VisionUpdate( + new Pose2d(latestVisionUpdate.getValue().visionPose.getTranslation(), rotation), + new Pose2d(latestVisionUpdate.getValue().odometryPose.getTranslation(), rotation)); + m_visionUpdates.put(latestVisionUpdate.getKey(), visionUpdate); + m_poseEstimate = visionUpdate.compensate(m_odometry.getPose()); + } else { + m_poseEstimate = m_odometry.getPose(); + } } /** @@ -274,29 +305,23 @@ public class PoseEstimator { var transform = visionRobotPose.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(timestamp, visionUpdate); - // Step 8: Remove later vision measurements. (Matches previous behavior) + // Step 7: Remove later vision measurements. (Matches previous behavior) m_visionUpdates.tailMap(timestamp, 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.getPose()); } diff --git a/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator3d.java b/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator3d.java index d9ab292d2a..4442102f19 100644 --- a/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator3d.java +++ b/wpimath/src/main/java/org/wpilib/math/estimator/PoseEstimator3d.java @@ -18,12 +18,9 @@ import org.wpilib.math.interpolation.TimeInterpolatableBuffer; import org.wpilib.math.kinematics.Kinematics; import org.wpilib.math.kinematics.Odometry3d; import org.wpilib.math.linalg.Matrix; -import org.wpilib.math.linalg.VecBuilder; import org.wpilib.math.numbers.N1; import org.wpilib.math.numbers.N4; -import org.wpilib.math.numbers.N6; import org.wpilib.math.util.MathSharedStore; -import org.wpilib.math.util.Nat; /** * This class wraps {@link Odometry3d} to fuse latency-compensated vision measurements with encoder @@ -46,8 +43,12 @@ import org.wpilib.math.util.Nat; */ public class PoseEstimator3d { private final Odometry3d m_odometry; - private final Matrix m_q = new Matrix<>(Nat.N4(), Nat.N1()); - private final Matrix 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 @@ -55,7 +56,9 @@ public class PoseEstimator3d { TimeInterpolatableBuffer.createBuffer(kBufferDuration); // Maps timestamps to vision updates // Always contains one entry before the oldest entry in m_odometryPoseBuffer, unless there have - // been no vision measurements after the last reset + // been no vision measurements after the last reset. May contain one entry while + // m_odometryPoseBuffer is empty to correct for translation/rotation after a call to + // ResetRotation/ResetTranslation. private final NavigableMap m_visionUpdates = new TreeMap<>(); private Pose3d m_poseEstimate; @@ -83,9 +86,10 @@ public class PoseEstimator3d { m_poseEstimate = m_odometry.getPose(); 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("PoseEstimator3d", ""); } /** @@ -98,6 +102,7 @@ public class PoseEstimator3d { * theta]ᵀ, with units in meters and radians. */ public final void setVisionMeasurementStdDevs(Matrix 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); @@ -106,17 +111,16 @@ public class PoseEstimator3d { // 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; } /** @@ -156,9 +160,22 @@ public class PoseEstimator3d { */ public void resetTranslation(Translation3d translation) { m_odometry.resetTranslation(translation); + + final var latestVisionUpdate = m_visionUpdates.lastEntry(); m_odometryPoseBuffer.clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.getPose(); + + if (latestVisionUpdate != null) { + // apply vision compensation to the pose rotation + final var visionUpdate = + new VisionUpdate( + new Pose3d(translation, latestVisionUpdate.getValue().visionPose.getRotation()), + new Pose3d(translation, latestVisionUpdate.getValue().odometryPose.getRotation())); + m_visionUpdates.put(latestVisionUpdate.getKey(), visionUpdate); + m_poseEstimate = visionUpdate.compensate(m_odometry.getPose()); + } else { + m_poseEstimate = m_odometry.getPose(); + } } /** @@ -168,9 +185,22 @@ public class PoseEstimator3d { */ public void resetRotation(Rotation3d rotation) { m_odometry.resetRotation(rotation); + + final var latestVisionUpdate = m_visionUpdates.lastEntry(); m_odometryPoseBuffer.clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.getPose(); + + if (latestVisionUpdate != null) { + // apply vision compensation to the pose translation + final var visionUpdate = + new VisionUpdate( + new Pose3d(latestVisionUpdate.getValue().visionPose.getTranslation(), rotation), + new Pose3d(latestVisionUpdate.getValue().odometryPose.getTranslation(), rotation)); + m_visionUpdates.put(latestVisionUpdate.getKey(), visionUpdate); + m_poseEstimate = visionUpdate.compensate(m_odometry.getPose()); + } else { + m_poseEstimate = m_odometry.getPose(); + } } /** @@ -286,38 +316,27 @@ public class PoseEstimator3d { var transform = visionRobotPose.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(timestamp, visionUpdate); - // Step 8: Remove later vision measurements. (Matches previous behavior) + // Step 7: Remove later vision measurements. (Matches previous behavior) m_visionUpdates.tailMap(timestamp, 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.getPose()); } diff --git a/wpimath/src/main/java/org/wpilib/math/estimator/SteadyStateKalmanFilter.java b/wpimath/src/main/java/org/wpilib/math/estimator/SteadyStateKalmanFilter.java index 7aad50ef2b..5717ea96fc 100644 --- a/wpimath/src/main/java/org/wpilib/math/estimator/SteadyStateKalmanFilter.java +++ b/wpimath/src/main/java/org/wpilib/math/estimator/SteadyStateKalmanFilter.java @@ -9,6 +9,7 @@ import org.wpilib.math.linalg.Matrix; import org.wpilib.math.numbers.N1; import org.wpilib.math.system.Discretization; import org.wpilib.math.system.LinearSystem; +import org.wpilib.math.util.MathSharedStore; import org.wpilib.math.util.Nat; import org.wpilib.math.util.Num; import org.wpilib.math.util.StateSpaceUtil; @@ -107,6 +108,7 @@ public class SteadyStateKalmanFilter(S.getStorage().solve(C.times(P).getStorage()).transpose()); reset(); + MathSharedStore.getMathShared().reportUsage("SteadyStateKalmanFilter", ""); } /** Resets the observer. */ diff --git a/wpimath/src/main/java/org/wpilib/math/estimator/UnscentedKalmanFilter.java b/wpimath/src/main/java/org/wpilib/math/estimator/UnscentedKalmanFilter.java index 0391bf8ef3..a502ddf988 100644 --- a/wpimath/src/main/java/org/wpilib/math/estimator/UnscentedKalmanFilter.java +++ b/wpimath/src/main/java/org/wpilib/math/estimator/UnscentedKalmanFilter.java @@ -12,6 +12,7 @@ import org.wpilib.math.numbers.N1; import org.wpilib.math.system.Discretization; import org.wpilib.math.system.NumericalIntegration; import org.wpilib.math.system.NumericalJacobian; +import org.wpilib.math.util.MathSharedStore; import org.wpilib.math.util.Nat; import org.wpilib.math.util.Num; import org.wpilib.math.util.Pair; @@ -173,6 +174,7 @@ public class UnscentedKalmanFilter Pair, Matrix> squareRootUnscentedTransform( diff --git a/wpimath/src/main/java/org/wpilib/math/filter/EdgeCounterFilter.java b/wpimath/src/main/java/org/wpilib/math/filter/EdgeCounterFilter.java new file mode 100644 index 0000000000..2b0fe0001d --- /dev/null +++ b/wpimath/src/main/java/org/wpilib/math/filter/EdgeCounterFilter.java @@ -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 org.wpilib.math.filter; + +import org.wpilib.math.util.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. + * + *

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. + */ +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; + } +} diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/CoordinateSystem.java b/wpimath/src/main/java/org/wpilib/math/geometry/CoordinateSystem.java index f16da87aa7..af65808c5c 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/CoordinateSystem.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/CoordinateSystem.java @@ -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))); } } diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/Pose2d.java b/wpimath/src/main/java/org/wpilib/math/geometry/Pose2d.java index 4bfecd57c6..460cd85261 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/Pose2d.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/Pose2d.java @@ -220,7 +220,7 @@ public class Pose2d implements Interpolatable, 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)); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/Pose3d.java b/wpimath/src/main/java/org/wpilib/math/geometry/Pose3d.java index 759de1afba..1db6cdd75a 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/Pose3d.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/Pose3d.java @@ -253,9 +253,12 @@ public class Pose3d implements Interpolatable, 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)); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/Rotation2d.java b/wpimath/src/main/java/org/wpilib/math/geometry/Rotation2d.java index 805ddf8eed..0525bb8a96 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/Rotation2d.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/Rotation2d.java @@ -264,6 +264,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. * diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/Rotation3d.java b/wpimath/src/main/java/org/wpilib/math/geometry/Rotation3d.java index c6980f670f..ce24c36efa 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/Rotation3d.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/Rotation3d.java @@ -25,7 +25,49 @@ import org.wpilib.units.measure.Angle; import org.wpilib.util.protobuf.ProtobufSerializable; import org.wpilib.util.struct.StructSerializable; -/** 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: + * + *

+ *          ┌───┐        ┌───┐
+ *          │ X │        │ x │
+ *   Front: │ | │  Back: │ · │
+ *          │ | │        │ · │
+ *          └───┘        └───┘
+ * 
+ * + *

If we rotate 90º clockwise around the axis into the page, then flip around the left/right + * axis, we get one result: + * + *

+ *   ┌───┐
+ *   │ X │   ┌───────┐   ┌───────┐
+ *   │ | │ → │------X│ → │······x│
+ *   │ | │   └───────┘   └───────┘
+ *   └───┘
+ * 
+ * + *

If we flip around the left/right axis, then rotate 90º clockwise around the axis into the + * page, we get a different result: + * + *

+ *   ┌───┐   ┌───┐
+ *   │ X │   │ · │   ┌───────┐
+ *   │ | │ → │ · │ → │x······│
+ *   │ | │   │ x │   └───────┘
+ *   └───┘   └───┘
+ * 
+ * + *

Because order matters for 3D rotations, we need to distinguish between extrinsic + * rotations and intrinsic 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 @@ -285,9 +327,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. + *

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) { @@ -295,10 +345,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. + * + *

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()); @@ -357,6 +416,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. * @@ -561,7 +638,7 @@ public class Rotation3d @Override public Rotation3d interpolate(Rotation3d endValue, double t) { - return plus(endValue.minus(this).times(Math.clamp(t, 0, 1))); + return endValue.minus(this).times(Math.clamp(t, 0, 1)).plus(this); } /** Rotation3d protobuf for serialization. */ diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/Transform2d.java b/wpimath/src/main/java/org/wpilib/math/geometry/Transform2d.java index 48cf673471..3eb461e419 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/Transform2d.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/Transform2d.java @@ -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()); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/geometry/Transform3d.java b/wpimath/src/main/java/org/wpilib/math/geometry/Transform3d.java index 5da26ed3bb..e65e2dd2f2 100644 --- a/wpimath/src/main/java/org/wpilib/math/geometry/Transform3d.java +++ b/wpimath/src/main/java/org/wpilib/math/geometry/Transform3d.java @@ -18,7 +18,10 @@ import org.wpilib.units.measure.Distance; import org.wpilib.util.protobuf.ProtobufSerializable; import org.wpilib.util.struct.StructSerializable; -/** 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. @@ -37,15 +40,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()); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry.java b/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry.java index 79dcd243cf..1e6c0ad407 100644 --- a/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry.java +++ b/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry.java @@ -24,7 +24,10 @@ public class Odometry { private Pose2d m_pose; private Rotation2d m_gyroOffset; + + // Always equal to m_poseMeters.getRotation() private Rotation2d m_previousAngle; + private final T m_previousWheelPositions; /** @@ -39,7 +42,7 @@ public class Odometry { Kinematics kinematics, Rotation2d gyroAngle, T wheelPositions, Pose2d initialPose) { m_kinematics = kinematics; m_pose = initialPose; - m_gyroOffset = m_pose.getRotation().minus(gyroAngle); + m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_pose.getRotation()); m_previousAngle = m_pose.getRotation(); m_previousWheelPositions = m_kinematics.copy(wheelPositions); } @@ -56,8 +59,8 @@ public class Odometry { */ public void resetPosition(Rotation2d gyroAngle, T wheelPositions, Pose2d pose) { m_pose = pose; + m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_pose.getRotation()); m_previousAngle = m_pose.getRotation(); - m_gyroOffset = m_pose.getRotation().minus(gyroAngle); m_kinematics.copyInto(wheelPositions, m_previousWheelPositions); } @@ -67,7 +70,8 @@ public class Odometry { * @param pose The pose to reset to. */ public void resetPose(Pose2d pose) { - m_gyroOffset = m_gyroOffset.plus(pose.getRotation().minus(m_pose.getRotation())); + m_gyroOffset = + m_gyroOffset.rotateBy(m_pose.getRotation().unaryMinus()).rotateBy(pose.getRotation()); m_pose = pose; m_previousAngle = m_pose.getRotation(); } @@ -87,7 +91,7 @@ public class Odometry { * @param rotation The rotation to reset to. */ public void resetRotation(Rotation2d rotation) { - m_gyroOffset = m_gyroOffset.plus(rotation.minus(m_pose.getRotation())); + m_gyroOffset = m_gyroOffset.rotateBy(m_pose.getRotation().unaryMinus()).rotateBy(rotation); m_pose = new Pose2d(m_pose.getTranslation(), rotation); m_previousAngle = m_pose.getRotation(); } @@ -112,7 +116,7 @@ public class Odometry { * @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(); diff --git a/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry3d.java b/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry3d.java index dd68b4d7d3..c2554dc9cc 100644 --- a/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry3d.java +++ b/wpimath/src/main/java/org/wpilib/math/kinematics/Odometry3d.java @@ -32,8 +32,15 @@ public class Odometry3d { private final Kinematics m_kinematics; private Pose3d m_pose; + // 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; /** @@ -48,7 +55,9 @@ public class Odometry3d { Kinematics kinematics, Rotation3d gyroAngle, T wheelPositions, Pose3d initialPose) { m_kinematics = kinematics; m_pose = initialPose; - m_gyroOffset = m_pose.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_pose.getRotation()); m_previousAngle = m_pose.getRotation(); m_previousWheelPositions = m_kinematics.copy(wheelPositions); } @@ -65,8 +74,10 @@ public class Odometry3d { */ public void resetPosition(Rotation3d gyroAngle, T wheelPositions, Pose3d pose) { m_pose = pose; + // When applied extrinsically, m_gyroOffset cancels the current gyroAngle and + // then rotates to m_poseMeters.getRotation() + m_gyroOffset = gyroAngle.unaryMinus().rotateBy(m_pose.getRotation()); m_previousAngle = m_pose.getRotation(); - m_gyroOffset = m_pose.getRotation().minus(gyroAngle); m_kinematics.copyInto(wheelPositions, m_previousWheelPositions); } @@ -76,7 +87,9 @@ public class Odometry3d { * @param pose The pose to reset to. */ public void resetPose(Pose3d pose) { - m_gyroOffset = m_gyroOffset.plus(pose.getRotation().minus(m_pose.getRotation())); + // Cancel the previous m_pose.Rotation() and then rotate to the new angle + m_gyroOffset = + m_gyroOffset.rotateBy(m_pose.getRotation().unaryMinus()).rotateBy(pose.getRotation()); m_pose = pose; m_previousAngle = m_pose.getRotation(); } @@ -96,7 +109,8 @@ public class Odometry3d { * @param rotation The rotation to reset to. */ public void resetRotation(Rotation3d rotation) { - m_gyroOffset = m_gyroOffset.plus(rotation.minus(m_pose.getRotation())); + // Cancel the previous m_pose.Rotation() and then rotate to the new angle + m_gyroOffset = m_gyroOffset.rotateBy(m_pose.getRotation().unaryMinus()).rotateBy(rotation); m_pose = new Pose3d(m_pose.getTranslation(), rotation); m_previousAngle = m_pose.getRotation(); } @@ -121,7 +135,7 @@ public class Odometry3d { * @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); diff --git a/wpimath/src/main/java/org/wpilib/math/system/LinearSystemLoop.java b/wpimath/src/main/java/org/wpilib/math/system/LinearSystemLoop.java index dccc4089f5..17f424c9ad 100644 --- a/wpimath/src/main/java/org/wpilib/math/system/LinearSystemLoop.java +++ b/wpimath/src/main/java/org/wpilib/math/system/LinearSystemLoop.java @@ -12,6 +12,7 @@ import org.wpilib.math.controller.LinearQuadraticRegulator; import org.wpilib.math.estimator.KalmanFilter; import org.wpilib.math.linalg.Matrix; import org.wpilib.math.numbers.N1; +import org.wpilib.math.util.MathSharedStore; import org.wpilib.math.util.Num; import org.wpilib.math.util.StateSpaceUtil; @@ -128,6 +129,7 @@ public class LinearSystemLoop(new SimpleMatrix(controller.getK().getNumCols(), 1)); reset(m_nextR); + MathSharedStore.getMathShared().reportUsage("LinearSystemLoop", ""); } /** diff --git a/wpimath/src/main/java/org/wpilib/math/trajectory/TrapezoidProfile.java b/wpimath/src/main/java/org/wpilib/math/trajectory/TrapezoidProfile.java index ee265b78e9..bb35f72480 100644 --- a/wpimath/src/main/java/org/wpilib/math/trajectory/TrapezoidProfile.java +++ b/wpimath/src/main/java/org/wpilib/math/trajectory/TrapezoidProfile.java @@ -7,6 +7,7 @@ package org.wpilib.math.trajectory; import java.util.Objects; import org.wpilib.math.trajectory.struct.TrapezoidProfileStateStruct; import org.wpilib.math.util.MathSharedStore; +import org.wpilib.util.struct.StructSerializable; /** * A trapezoid-shaped velocity profile. @@ -75,7 +76,7 @@ public class TrapezoidProfile { } /** Profile state. */ - public static class State { + public static class State implements StructSerializable { /** The struct used to serialize this class. */ public static final TrapezoidProfileStateStruct struct = new TrapezoidProfileStateStruct(); diff --git a/wpimath/src/main/native/cpp/filter/EdgeCounterFilter.cpp b/wpimath/src/main/native/cpp/filter/EdgeCounterFilter.cpp new file mode 100644 index 0000000000..6996602cef --- /dev/null +++ b/wpimath/src/main/native/cpp/filter/EdgeCounterFilter.cpp @@ -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 "wpi/math/filter/EdgeCounterFilter.hpp" + +#include "wpi/math/util/MathShared.hpp" + +using namespace wpi::math; + +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; +} diff --git a/wpimath/src/main/native/include/wpi/math/controller/LinearQuadraticRegulator.hpp b/wpimath/src/main/native/include/wpi/math/controller/LinearQuadraticRegulator.hpp index 5a849cfd17..65bfe20476 100644 --- a/wpimath/src/main/native/include/wpi/math/controller/LinearQuadraticRegulator.hpp +++ b/wpimath/src/main/native/include/wpi/math/controller/LinearQuadraticRegulator.hpp @@ -135,6 +135,7 @@ class LinearQuadraticRegulator { } Reset(); + wpi::math::MathSharedStore::ReportUsage("LinearQuadraticRegulator", ""); } /** @@ -194,6 +195,7 @@ class LinearQuadraticRegulator { } Reset(); + wpi::math::MathSharedStore::ReportUsage("LinearQuadraticRegulator", ""); } LinearQuadraticRegulator(LinearQuadraticRegulator&&) = default; diff --git a/wpimath/src/main/native/include/wpi/math/estimator/ExtendedKalmanFilter.hpp b/wpimath/src/main/native/include/wpi/math/estimator/ExtendedKalmanFilter.hpp index 3748cdad00..12d29fd7c3 100644 --- a/wpimath/src/main/native/include/wpi/math/estimator/ExtendedKalmanFilter.hpp +++ b/wpimath/src/main/native/include/wpi/math/estimator/ExtendedKalmanFilter.hpp @@ -138,6 +138,7 @@ class ExtendedKalmanFilter { m_initP = StateMatrix::Zero(); } m_P = m_initP; + wpi::math::MathSharedStore::ReportUsage("ExtendedKalmanFilter", ""); } /** @@ -222,6 +223,7 @@ class ExtendedKalmanFilter { m_initP = StateMatrix::Zero(); } m_P = m_initP; + wpi::math::MathSharedStore::ReportUsage("ExtendedKalmanFilter", ""); } /** diff --git a/wpimath/src/main/native/include/wpi/math/estimator/KalmanFilter.hpp b/wpimath/src/main/native/include/wpi/math/estimator/KalmanFilter.hpp index d11a230aea..3a6445c02d 100644 --- a/wpimath/src/main/native/include/wpi/math/estimator/KalmanFilter.hpp +++ b/wpimath/src/main/native/include/wpi/math/estimator/KalmanFilter.hpp @@ -117,6 +117,7 @@ class KalmanFilter { } Reset(); + wpi::math::MathSharedStore::ReportUsage("KalmanFilter", ""); } /** diff --git a/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator.hpp b/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator.hpp index d093efe096..d73d706be8 100644 --- a/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator.hpp +++ b/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator.hpp @@ -74,6 +74,7 @@ class WPILIB_DLLEXPORT PoseEstimator { } SetVisionMeasurementStdDevs(visionMeasurementStdDevs); + wpi::math::MathSharedStore::ReportUsage("PoseEstimator", ""); } /** @@ -88,6 +89,7 @@ class WPILIB_DLLEXPORT PoseEstimator { */ void SetVisionMeasurementStdDevs( const wpi::util::array& visionMeasurementStdDevs) { + // Diagonal of measurement covariance matrix R wpi::util::array r{wpi::util::empty_array}; for (size_t i = 0; i < 3; ++i) { r[i] = visionMeasurementStdDevs[i] * visionMeasurementStdDevs[i]; @@ -97,9 +99,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])); } } @@ -141,24 +143,70 @@ class WPILIB_DLLEXPORT PoseEstimator { * * @param translation The pose to translation to. */ +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif // defined(__GNUC__) && !defined(__clang__) void ResetTranslation(const Translation2d& translation) { m_odometry.ResetTranslation(translation); + + const std::optional> + latestVisionUpdate = + m_visionUpdates.empty() ? std::nullopt + : std::optional{*m_visionUpdates.crbegin()}; m_odometryPoseBuffer.Clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.GetPose(); + + if (latestVisionUpdate) { + // apply vision compensation to the pose rotation + const VisionUpdate visionUpdate{ + Pose2d{translation, latestVisionUpdate->second.visionPose.Rotation()}, + Pose2d{translation, + latestVisionUpdate->second.odometryPose.Rotation()}}; + m_visionUpdates[latestVisionUpdate->first] = visionUpdate; + m_poseEstimate = visionUpdate.Compensate(m_odometry.GetPose()); + } else { + m_poseEstimate = m_odometry.GetPose(); + } } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif // defined(__GNUC__) && !defined(__clang__) /** * Resets the robot's rotation. * * @param rotation The rotation to reset to. */ +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif // defined(__GNUC__) && !defined(__clang__) void ResetRotation(const Rotation2d& rotation) { m_odometry.ResetRotation(rotation); + + const std::optional> + latestVisionUpdate = + m_visionUpdates.empty() ? std::nullopt + : std::optional{*m_visionUpdates.crbegin()}; m_odometryPoseBuffer.Clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.GetPose(); + + if (latestVisionUpdate) { + // apply vision compensation to the pose translation + const VisionUpdate visionUpdate{ + Pose2d{latestVisionUpdate->second.visionPose.Translation(), rotation}, + Pose2d{latestVisionUpdate->second.odometryPose.Translation(), + rotation}}; + m_visionUpdates[latestVisionUpdate->first] = visionUpdate; + m_poseEstimate = visionUpdate.Compensate(m_odometry.GetPose()); + } else { + m_poseEstimate = m_odometry.GetPose(); + } } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif // defined(__GNUC__) && !defined(__clang__) /** * Gets the estimated robot pose. @@ -274,9 +322,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{ @@ -431,14 +479,21 @@ class WPILIB_DLLEXPORT PoseEstimator { static constexpr wpi::units::second_t kBufferDuration = 1.5_s; Odometry& m_odometry; + + // Diagonal of process noise covariance matrix Q wpi::util::array m_q{wpi::util::empty_array}; - Eigen::Matrix3d m_visionK = Eigen::Matrix3d::Zero(); + + // Kalman gain matrix K + Eigen::DiagonalMatrix m_vision_K = + Eigen::DiagonalMatrix::Zero(); // Maps timestamps to odometry-only pose estimates TimeInterpolatableBuffer m_odometryPoseBuffer{kBufferDuration}; // Maps timestamps to vision updates // Always contains one entry before the oldest entry in m_odometryPoseBuffer, - // unless there have been no vision measurements after the last reset + // unless there have been no vision measurements after the last reset. May + // contain one entry while m_odometryPoseBuffer is empty to correct for + // translation/rotation after a call to ResetRotation/ResetTranslation. std::map m_visionUpdates; Pose2d m_poseEstimate; diff --git a/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator3d.hpp b/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator3d.hpp index 1cd77c0d9d..c531630383 100644 --- a/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator3d.hpp +++ b/wpimath/src/main/native/include/wpi/math/estimator/PoseEstimator3d.hpp @@ -79,6 +79,7 @@ class WPILIB_DLLEXPORT PoseEstimator3d { } SetVisionMeasurementStdDevs(visionMeasurementStdDevs); + wpi::math::MathSharedStore::ReportUsage("PoseEstimator3d", ""); } /** @@ -93,6 +94,7 @@ class WPILIB_DLLEXPORT PoseEstimator3d { */ void SetVisionMeasurementStdDevs( const wpi::util::array& visionMeasurementStdDevs) { + // Diagonal of measurement noise covariance matrix R wpi::util::array r{wpi::util::empty_array}; for (size_t i = 0; i < 4; ++i) { r[i] = visionMeasurementStdDevs[i] * visionMeasurementStdDevs[i]; @@ -102,15 +104,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; } /** @@ -149,24 +151,70 @@ class WPILIB_DLLEXPORT PoseEstimator3d { * * @param translation The pose to translation to. */ +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif // defined(__GNUC__) && !defined(__clang__) void ResetTranslation(const Translation3d& translation) { m_odometry.ResetTranslation(translation); + + const std::optional> + latestVisionUpdate = + m_visionUpdates.empty() ? std::nullopt + : std::optional{*m_visionUpdates.crbegin()}; m_odometryPoseBuffer.Clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.GetPose(); + + if (latestVisionUpdate) { + // apply vision compensation to the pose rotation + const VisionUpdate visionUpdate{ + Pose3d{translation, latestVisionUpdate->second.visionPose.Rotation()}, + Pose3d{translation, + latestVisionUpdate->second.odometryPose.Rotation()}}; + m_visionUpdates[latestVisionUpdate->first] = visionUpdate; + m_poseEstimate = visionUpdate.Compensate(m_odometry.GetPose()); + } else { + m_poseEstimate = m_odometry.GetPose(); + } } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif // defined(__GNUC__) && !defined(__clang__) /** * Resets the robot's rotation. * * @param rotation The rotation to reset to. */ +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif // defined(__GNUC__) && !defined(__clang__) void ResetRotation(const Rotation3d& rotation) { m_odometry.ResetRotation(rotation); + + const std::optional> + latestVisionUpdate = + m_visionUpdates.empty() ? std::nullopt + : std::optional{*m_visionUpdates.crbegin()}; m_odometryPoseBuffer.Clear(); m_visionUpdates.clear(); - m_poseEstimate = m_odometry.GetPose(); + + if (latestVisionUpdate) { + // apply vision compensation to the pose translation + const VisionUpdate visionUpdate{ + Pose3d{latestVisionUpdate->second.visionPose.Translation(), rotation}, + Pose3d{latestVisionUpdate->second.odometryPose.Translation(), + rotation}}; + m_visionUpdates[latestVisionUpdate->first] = visionUpdate; + m_poseEstimate = visionUpdate.Compensate(m_odometry.GetPose()); + } else { + m_poseEstimate = m_odometry.GetPose(); + } } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif // defined(__GNUC__) && !defined(__clang__) /** * Gets the estimated robot pose. @@ -282,12 +330,12 @@ class WPILIB_DLLEXPORT PoseEstimator3d { // this transform by a Kalman gain matrix representing how much we trust // vision measurements compared to our current pose. wpi::math::Vectord<6> k_times_transform = - m_visionK * wpi::math::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 * wpi::math::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{ @@ -445,14 +493,21 @@ class WPILIB_DLLEXPORT PoseEstimator3d { static constexpr wpi::units::second_t kBufferDuration = 1.5_s; Odometry3d& m_odometry; + + // Diagonal of process noise covariance matrix Q wpi::util::array m_q{wpi::util::empty_array}; - wpi::math::Matrixd<6, 6> m_visionK = wpi::math::Matrixd<6, 6>::Zero(); + + // Kalman gain matrix K + Eigen::DiagonalMatrix m_vision_K = + Eigen::DiagonalMatrix::Zero(); // Maps timestamps to odometry-only pose estimates TimeInterpolatableBuffer m_odometryPoseBuffer{kBufferDuration}; // Maps timestamps to vision updates // Always contains one entry before the oldest entry in m_odometryPoseBuffer, - // unless there have been no vision measurements after the last reset + // unless there have been no vision measurements after the last reset. May + // contain one entry while m_odometryPoseBuffer is empty to correct for + // translation/rotation after a call to ResetRotation/ResetTranslation. std::map m_visionUpdates; Pose3d m_poseEstimate; diff --git a/wpimath/src/main/native/include/wpi/math/estimator/SteadyStateKalmanFilter.hpp b/wpimath/src/main/native/include/wpi/math/estimator/SteadyStateKalmanFilter.hpp index a748f5bc22..613c0a3eb3 100644 --- a/wpimath/src/main/native/include/wpi/math/estimator/SteadyStateKalmanFilter.hpp +++ b/wpimath/src/main/native/include/wpi/math/estimator/SteadyStateKalmanFilter.hpp @@ -148,6 +148,7 @@ class SteadyStateKalmanFilter { } Reset(); + wpi::math::MathSharedStore::ReportUsage("KalmanFilter", ""); } SteadyStateKalmanFilter(SteadyStateKalmanFilter&&) = default; diff --git a/wpimath/src/main/native/include/wpi/math/estimator/UnscentedKalmanFilter.hpp b/wpimath/src/main/native/include/wpi/math/estimator/UnscentedKalmanFilter.hpp index 3628cb3c67..b8afbe7433 100644 --- a/wpimath/src/main/native/include/wpi/math/estimator/UnscentedKalmanFilter.hpp +++ b/wpimath/src/main/native/include/wpi/math/estimator/UnscentedKalmanFilter.hpp @@ -15,6 +15,7 @@ #include "wpi/math/system/Discretization.hpp" #include "wpi/math/system/NumericalIntegration.hpp" #include "wpi/math/system/NumericalJacobian.hpp" +#include "wpi/math/util/MathShared.hpp" #include "wpi/math/util/StateSpaceUtil.hpp" #include "wpi/units/time.hpp" #include "wpi/util/array.hpp" @@ -114,6 +115,7 @@ class UnscentedKalmanFilter { m_dt = dt; Reset(); + wpi::math::MathSharedStore::ReportUsage("UnscentedKalmanFilter", ""); } /** @@ -173,6 +175,7 @@ class UnscentedKalmanFilter { m_dt = dt; Reset(); + wpi::math::MathSharedStore::ReportUsage("UnscentedKalmanFilter", ""); } /** diff --git a/wpimath/src/main/native/include/wpi/math/filter/EdgeCounterFilter.hpp b/wpimath/src/main/native/include/wpi/math/filter/EdgeCounterFilter.hpp new file mode 100644 index 0000000000..ce4dbed6b3 --- /dev/null +++ b/wpimath/src/main/native/include/wpi/math/filter/EdgeCounterFilter.hpp @@ -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/units/time.hpp" +#include "wpi/util/SymbolExports.hpp" + +namespace wpi::math { +/** + * 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, + wpi::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(wpi::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 wpi::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; + wpi::units::second_t m_windowTime; + + wpi::units::second_t m_firstEdgeTime; + int m_currentCount = 0; + + bool m_lastInput = false; + + void ResetTimer(); + + bool HasElapsed() const; +}; +} // namespace wpi::math diff --git a/wpimath/src/main/native/include/wpi/math/geometry/CoordinateSystem.hpp b/wpimath/src/main/native/include/wpi/math/geometry/CoordinateSystem.hpp index ff3fd7ebbd..353a76faa3 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/CoordinateSystem.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/CoordinateSystem.hpp @@ -85,7 +85,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); } /** @@ -99,7 +100,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); } /** @@ -128,14 +130,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; }; diff --git a/wpimath/src/main/native/include/wpi/math/geometry/Pose2d.hpp b/wpimath/src/main/native/include/wpi/math/geometry/Pose2d.hpp index 3e2c3b8661..d716def254 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/Pose2d.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/Pose2d.hpp @@ -286,8 +286,8 @@ constexpr Transform2d Pose2d::operator-(const Pose2d& other) const { constexpr Pose2d Pose2d::TransformBy( const wpi::math::Transform2d& other) const { - return {m_translation + other.Translation().RotateBy(m_rotation), - other.Rotation() + m_rotation}; + return {m_translation + (other.Translation().RotateBy(m_rotation)), + other.Rotation().RotateBy(m_rotation)}; } constexpr Pose2d Pose2d::RelativeTo(const Pose2d& other) const { diff --git a/wpimath/src/main/native/include/wpi/math/geometry/Pose3d.hpp b/wpimath/src/main/native/include/wpi/math/geometry/Pose3d.hpp index c19d786e11..9963ef4150 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/Pose3d.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/Pose3d.hpp @@ -312,8 +312,11 @@ constexpr Transform3d Pose3d::operator-(const Pose3d& other) const { } constexpr Pose3d Pose3d::TransformBy(const Transform3d& other) const { - return {m_translation + other.Translation().RotateBy(m_rotation), - other.Rotation() + m_rotation}; + // 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().RotateBy(m_rotation)}; } constexpr Pose3d Pose3d::RelativeTo(const Pose3d& other) const { diff --git a/wpimath/src/main/native/include/wpi/math/geometry/Rotation2d.hpp b/wpimath/src/main/native/include/wpi/math/geometry/Rotation2d.hpp index d4897bc93d..8f77a2e0e3 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/Rotation2d.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/Rotation2d.hpp @@ -190,6 +190,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. */ diff --git a/wpimath/src/main/native/include/wpi/math/geometry/Rotation3d.hpp b/wpimath/src/main/native/include/wpi/math/geometry/Rotation3d.hpp index 5a1d9368b8..f1430ad744 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/Rotation3d.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/Rotation3d.hpp @@ -15,13 +15,56 @@ #include "wpi/math/linalg/ct_matrix.hpp" #include "wpi/units/angle.hpp" #include "wpi/units/math.hpp" +#include "wpi/util/MathExtras.hpp" #include "wpi/util/SymbolExports.hpp" #include "wpi/util/json_fwd.hpp" namespace wpi::math { /** - * 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: + * + *

+ *          ┌───┐        ┌───┐
+ *          │ X │        │ x │
+ *   Front: │ | │  Back: │ · │
+ *          │ | │        │ · │
+ *          └───┘        └───┘
+ * 
+ * + * If we rotate 90º clockwise around the axis into the page, then flip around + * the left/right axis, we get one result: + * + *
+ *   ┌───┐
+ *   │ X │   ┌───────┐   ┌───────┐
+ *   │ | │ → │------X│ → │······x│
+ *   │ | │   └───────┘   └───────┘
+ *   └───┘
+ * 
+ * + * If we flip around the left/right axis, then rotate 90º clockwise around the + * axis into the page, we get a different result: + * + *
+ *   ┌───┐   ┌───┐
+ *   │ X │   │ · │   ┌───────┐
+ *   │ | │ → │ · │ → │x······│
+ *   │ | │   │ x │   └───────┘
+ *   └───┘   └───┘
+ * 
+ * + * Because order matters for 3D rotations, we need to distinguish between + * extrinsic rotations and intrinsic 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: @@ -240,9 +283,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. */ @@ -252,11 +304,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. */ @@ -450,5 +533,12 @@ void from_json(const wpi::util::json& json, Rotation3d& rotation); } // namespace wpi::math +template <> +constexpr wpi::math::Rotation3d wpi::util::Lerp( + const wpi::math::Rotation3d& startValue, + const wpi::math::Rotation3d& endValue, double t) { + return (endValue - startValue) * t + startValue; +} + #include "wpi/math/geometry/proto/Rotation3dProto.hpp" #include "wpi/math/geometry/struct/Rotation3dStruct.hpp" diff --git a/wpimath/src/main/native/include/wpi/math/geometry/Transform2d.hpp b/wpimath/src/main/native/include/wpi/math/geometry/Transform2d.hpp index 5c6be18b03..e828b31108 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/Transform2d.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/Transform2d.hpp @@ -177,13 +177,12 @@ class WPILIB_DLLEXPORT Transform2d { namespace wpi::math { 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 { diff --git a/wpimath/src/main/native/include/wpi/math/geometry/Transform3d.hpp b/wpimath/src/main/native/include/wpi/math/geometry/Transform3d.hpp index f88a032d1e..a353c64e5e 100644 --- a/wpimath/src/main/native/include/wpi/math/geometry/Transform3d.hpp +++ b/wpimath/src/main/native/include/wpi/math/geometry/Transform3d.hpp @@ -16,7 +16,9 @@ class Pose3d; struct Twist3d; /** - * 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: @@ -202,13 +204,12 @@ class WPILIB_DLLEXPORT Transform3d { namespace wpi::math { 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 { diff --git a/wpimath/src/main/native/include/wpi/math/interpolation/TimeInterpolatableBuffer.hpp b/wpimath/src/main/native/include/wpi/math/interpolation/TimeInterpolatableBuffer.hpp index 794706ab04..deb6043570 100644 --- a/wpimath/src/main/native/include/wpi/math/interpolation/TimeInterpolatableBuffer.hpp +++ b/wpimath/src/main/native/include/wpi/math/interpolation/TimeInterpolatableBuffer.hpp @@ -11,6 +11,8 @@ #include #include "wpi/math/geometry/Pose2d.hpp" +#include "wpi/math/geometry/Pose3d.hpp" +#include "wpi/math/geometry/Rotation3d.hpp" #include "wpi/units/time.hpp" #include "wpi/util/MathExtras.hpp" @@ -155,7 +157,8 @@ class TimeInterpolatableBuffer { std::function 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::TimeInterpolatableBuffer( wpi::units::second_t historySize) @@ -172,4 +175,20 @@ inline TimeInterpolatableBuffer::TimeInterpolatableBuffer( } }) {} +template <> +inline TimeInterpolatableBuffer::TimeInterpolatableBuffer( + wpi::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 = (end - start).Log(); + Twist3d scaledTwist = twist * t; + return start + scaledTwist.Exp(); + } + }) {} + } // namespace wpi::math diff --git a/wpimath/src/main/native/include/wpi/math/kinematics/Odometry.hpp b/wpimath/src/main/native/include/wpi/math/kinematics/Odometry.hpp index 478bed1d90..4fda914b0d 100644 --- a/wpimath/src/main/native/include/wpi/math/kinematics/Odometry.hpp +++ b/wpimath/src/main/native/include/wpi/math/kinematics/Odometry.hpp @@ -47,7 +47,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()); } /** @@ -64,7 +64,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; } @@ -74,7 +74,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(); } @@ -94,7 +95,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; } @@ -118,7 +119,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); @@ -139,7 +140,10 @@ class WPILIB_DLLEXPORT Odometry { Pose2d m_pose; WheelPositions m_previousWheelPositions; + + // Always equal to m_pose.Rotation() Rotation2d m_previousAngle; + Rotation2d m_gyroOffset; }; diff --git a/wpimath/src/main/native/include/wpi/math/kinematics/Odometry3d.hpp b/wpimath/src/main/native/include/wpi/math/kinematics/Odometry3d.hpp index f059633422..fca50ffeb2 100644 --- a/wpimath/src/main/native/include/wpi/math/kinematics/Odometry3d.hpp +++ b/wpimath/src/main/native/include/wpi/math/kinematics/Odometry3d.hpp @@ -47,7 +47,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()); } /** @@ -64,7 +66,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; } @@ -74,7 +78,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(); } @@ -94,7 +100,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; } @@ -118,7 +125,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; }; diff --git a/wpimath/src/main/native/include/wpi/math/system/LinearSystemLoop.hpp b/wpimath/src/main/native/include/wpi/math/system/LinearSystemLoop.hpp index fc99061e5a..a492d9f54b 100644 --- a/wpimath/src/main/native/include/wpi/math/system/LinearSystemLoop.hpp +++ b/wpimath/src/main/native/include/wpi/math/system/LinearSystemLoop.hpp @@ -131,6 +131,7 @@ class LinearSystemLoop { m_clampFunc(clampFunction) { m_nextR.setZero(); Reset(m_nextR); + wpi::math::MathSharedStore::ReportUsage("LinearSystemLoop", ""); } LinearSystemLoop(LinearSystemLoop&&) = default; diff --git a/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimator3dTest.java b/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimator3dTest.java index f77e77612e..270d95c94c 100644 --- a/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimator3dTest.java +++ b/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimator3dTest.java @@ -28,6 +28,7 @@ import org.wpilib.math.linalg.VecBuilder; import org.wpilib.math.trajectory.Trajectory; import org.wpilib.math.trajectory.TrajectoryConfig; import org.wpilib.math.trajectory.TrajectoryGenerator; +import org.wpilib.math.util.MathSharedStore; class SwerveDrivePoseEstimator3dTest { private static final double kEpsilon = 1e-9; @@ -499,11 +500,23 @@ class SwerveDrivePoseEstimator3dTest { () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getY(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getZ(), kEpsilon)); + // Add a vision measurement with a different translation + estimator.addVisionMeasurement( + new Pose3d(3, 0, 0, Rotation3d.kZero), MathSharedStore.getTimestamp()); + + assertAll( + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getY(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getZ(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getX(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getY(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getZ(), kEpsilon)); + // Test reset rotation estimator.resetRotation(new Rotation3d(Rotation2d.kCCW_Pi_2)); assertAll( - () -> assertEquals(2, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getY(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getZ(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getX(), kEpsilon), @@ -523,7 +536,7 @@ class SwerveDrivePoseEstimator3dTest { } assertAll( - () -> assertEquals(2, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), () -> assertEquals(1, estimator.getEstimatedPosition().getY(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getZ(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getX(), kEpsilon), @@ -532,6 +545,22 @@ class SwerveDrivePoseEstimator3dTest { assertEquals( Math.PI / 2, estimator.getEstimatedPosition().getRotation().getZ(), kEpsilon)); + // Add a vision measurement with a different rotation + estimator.addVisionMeasurement( + new Pose3d(2.5, 1, 0, new Rotation3d(Rotation2d.kPi)), MathSharedStore.getTimestamp()); + + assertAll( + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(1, estimator.getEstimatedPosition().getY(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getZ(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getX(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getY(), kEpsilon), + () -> + assertEquals( + Math.PI * 3.0 / 4, + estimator.getEstimatedPosition().getRotation().getZ(), + kEpsilon)); + // Test reset translation estimator.resetTranslation(new Translation3d(-1, -1, -1)); @@ -543,7 +572,9 @@ class SwerveDrivePoseEstimator3dTest { () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getY(), kEpsilon), () -> assertEquals( - Math.PI / 2, estimator.getEstimatedPosition().getRotation().getZ(), kEpsilon)); + Math.PI * 3.0 / 4, + estimator.getEstimatedPosition().getRotation().getZ(), + kEpsilon)); // Test reset pose estimator.resetPose(Pose3d.kZero); diff --git a/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimatorTest.java b/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimatorTest.java index 39275c5c11..0c36e72f00 100644 --- a/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimatorTest.java +++ b/wpimath/src/test/java/org/wpilib/math/estimator/SwerveDrivePoseEstimatorTest.java @@ -25,6 +25,7 @@ import org.wpilib.math.linalg.VecBuilder; import org.wpilib.math.trajectory.Trajectory; import org.wpilib.math.trajectory.TrajectoryConfig; import org.wpilib.math.trajectory.TrajectoryGenerator; +import org.wpilib.math.util.MathSharedStore; class SwerveDrivePoseEstimatorTest { private static final double kEpsilon = 1e-9; @@ -470,11 +471,21 @@ class SwerveDrivePoseEstimatorTest { () -> assertEquals(0, estimator.getEstimatedPosition().getRotation().getRadians(), kEpsilon)); + // Add a vision measurement with a different translation + estimator.addVisionMeasurement( + new Pose2d(3, 0, Rotation2d.kZero), MathSharedStore.getTimestamp()); + + assertAll( + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(0, estimator.getEstimatedPosition().getY(), kEpsilon), + () -> + assertEquals(0, estimator.getEstimatedPosition().getRotation().getRadians(), kEpsilon)); + // Test reset rotation estimator.resetRotation(Rotation2d.kCCW_Pi_2); assertAll( - () -> assertEquals(2, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), () -> assertEquals(0, estimator.getEstimatedPosition().getY(), kEpsilon), () -> assertEquals( @@ -493,7 +504,7 @@ class SwerveDrivePoseEstimatorTest { } assertAll( - () -> assertEquals(2, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), () -> assertEquals(1, estimator.getEstimatedPosition().getY(), kEpsilon), () -> assertEquals( @@ -501,6 +512,19 @@ class SwerveDrivePoseEstimatorTest { estimator.getEstimatedPosition().getRotation().getRadians(), kEpsilon)); + // Add a vision measurement with a different rotation + estimator.addVisionMeasurement( + new Pose2d(2.5, 1, Rotation2d.kPi), MathSharedStore.getTimestamp()); + + assertAll( + () -> assertEquals(2.5, estimator.getEstimatedPosition().getX(), kEpsilon), + () -> assertEquals(1, estimator.getEstimatedPosition().getY(), kEpsilon), + () -> + assertEquals( + Math.PI * 3.0 / 4, + estimator.getEstimatedPosition().getRotation().getRadians(), + kEpsilon)); + // Test reset translation estimator.resetTranslation(new Translation2d(-1, -1)); @@ -509,7 +533,7 @@ class SwerveDrivePoseEstimatorTest { () -> assertEquals(-1, estimator.getEstimatedPosition().getY(), kEpsilon), () -> assertEquals( - Math.PI / 2, + Math.PI * 3.0 / 4, estimator.getEstimatedPosition().getRotation().getRadians(), kEpsilon)); diff --git a/wpimath/src/test/java/org/wpilib/math/filter/EdgeCounterFilterTest.java b/wpimath/src/test/java/org/wpilib/math/filter/EdgeCounterFilterTest.java new file mode 100644 index 0000000000..c6d49a3ef6 --- /dev/null +++ b/wpimath/src/test/java/org/wpilib/math/filter/EdgeCounterFilterTest.java @@ -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 org.wpilib.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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.wpilib.util.WPIUtilJNI; + +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); + } +} diff --git a/wpimath/src/test/java/org/wpilib/math/geometry/Rotation2dTest.java b/wpimath/src/test/java/org/wpilib/math/geometry/Rotation2dTest.java index 3214300e8c..e3eaa5bdfe 100644 --- a/wpimath/src/test/java/org/wpilib/math/geometry/Rotation2dTest.java +++ b/wpimath/src/test/java/org/wpilib/math/geometry/Rotation2dTest.java @@ -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); diff --git a/wpimath/src/test/java/org/wpilib/math/geometry/Rotation3dTest.java b/wpimath/src/test/java/org/wpilib/math/geometry/Rotation3dTest.java index 54385ecce5..6ef74d535a 100644 --- a/wpimath/src/test/java/org/wpilib/math/geometry/Rotation3dTest.java +++ b/wpimath/src/test/java/org/wpilib/math/geometry/Rotation3dTest.java @@ -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); } } diff --git a/wpimath/src/test/java/org/wpilib/math/kinematics/DifferentialDriveOdometry3dTest.java b/wpimath/src/test/java/org/wpilib/math/kinematics/DifferentialDriveOdometry3dTest.java index 7c319eb7ac..5b830df0df 100644 --- a/wpimath/src/test/java/org/wpilib/math/kinematics/DifferentialDriveOdometry3dTest.java +++ b/wpimath/src/test/java/org/wpilib/math/kinematics/DifferentialDriveOdometry3dTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; import org.wpilib.math.geometry.Pose3d; import org.wpilib.math.geometry.Rotation3d; +import org.wpilib.math.geometry.Translation3d; import org.wpilib.math.util.Units; class DifferentialDriveOdometry3dTest { @@ -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)); + } } diff --git a/wpimath/src/test/java/org/wpilib/math/kinematics/MecanumDriveOdometry3dTest.java b/wpimath/src/test/java/org/wpilib/math/kinematics/MecanumDriveOdometry3dTest.java index 34b5438c72..da2f34f6cc 100644 --- a/wpimath/src/test/java/org/wpilib/math/kinematics/MecanumDriveOdometry3dTest.java +++ b/wpimath/src/test/java/org/wpilib/math/kinematics/MecanumDriveOdometry3dTest.java @@ -288,4 +288,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)); + } } diff --git a/wpimath/src/test/java/org/wpilib/math/kinematics/SwerveDriveOdometry3dTest.java b/wpimath/src/test/java/org/wpilib/math/kinematics/SwerveDriveOdometry3dTest.java index b4f30704d9..0c238e49e9 100644 --- a/wpimath/src/test/java/org/wpilib/math/kinematics/SwerveDriveOdometry3dTest.java +++ b/wpimath/src/test/java/org/wpilib/math/kinematics/SwerveDriveOdometry3dTest.java @@ -294,4 +294,22 @@ class SwerveDriveOdometry3dTest { assertEquals(0.0, errorSum / (trajectory.getTotalTime() / 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)); + } } diff --git a/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimator3dTest.cpp b/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimator3dTest.cpp index e16f62d4d8..3344c45cf5 100644 --- a/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimator3dTest.cpp +++ b/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimator3dTest.cpp @@ -498,10 +498,22 @@ TEST(SwerveDrivePoseEstimator3dTest, TestReset) { EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().Y().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().Z().value()); + // Add a vision measurement with a different translation + estimator.AddVisionMeasurement( + wpi::math::Pose3d(3_m, 0_m, 0_m, wpi::math::Rotation3d{}), + wpi::math::MathSharedStore::GetTimestamp()); + + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Y().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Z().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().X().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().Y().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().Z().value()); + // Test reset rotation estimator.ResetRotation(wpi::math::Rotation3d{0_deg, 0_deg, 90_deg}); - EXPECT_DOUBLE_EQ(2, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Y().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Z().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().X().value()); @@ -517,7 +529,7 @@ TEST(SwerveDrivePoseEstimator3dTest, TestReset) { modulePosition, modulePosition}); } - EXPECT_DOUBLE_EQ(2, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); EXPECT_DOUBLE_EQ(1, estimator.GetEstimatedPosition().Y().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Z().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().X().value()); @@ -525,6 +537,20 @@ TEST(SwerveDrivePoseEstimator3dTest, TestReset) { EXPECT_DOUBLE_EQ(std::numbers::pi / 2, estimator.GetEstimatedPosition().Rotation().Z().value()); + // Add a vision measurement with a different rotation + estimator.AddVisionMeasurement( + wpi::math::Pose3d(2.5_m, 1_m, 0_m, + wpi::math::Rotation3d{wpi::math::Rotation2d{180_deg}}), + wpi::math::MathSharedStore::GetTimestamp()); + + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(1, estimator.GetEstimatedPosition().Y().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Z().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().X().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().Y().value()); + EXPECT_DOUBLE_EQ(std::numbers::pi * 3.0 / 4, + estimator.GetEstimatedPosition().Rotation().Z().value()); + // Test reset translation estimator.ResetTranslation(wpi::math::Translation3d{-1_m, -1_m, -1_m}); @@ -533,7 +559,7 @@ TEST(SwerveDrivePoseEstimator3dTest, TestReset) { EXPECT_DOUBLE_EQ(-1, estimator.GetEstimatedPosition().Z().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().X().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Rotation().Y().value()); - EXPECT_DOUBLE_EQ(std::numbers::pi / 2, + EXPECT_DOUBLE_EQ(std::numbers::pi * 3.0 / 4, estimator.GetEstimatedPosition().Rotation().Z().value()); // Test reset pose diff --git a/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp b/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp index 527b97ed10..a154cc03ff 100644 --- a/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp +++ b/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp @@ -464,10 +464,20 @@ TEST(SwerveDrivePoseEstimatorTest, TestReset) { EXPECT_DOUBLE_EQ( 0, estimator.GetEstimatedPosition().Rotation().Radians().value()); + // Add a vision measurement with a different translation + estimator.AddVisionMeasurement( + wpi::math::Pose2d{3_m, 0_m, wpi::math::Rotation2d{}}, + wpi::math::MathSharedStore::GetTimestamp()); + + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Y().value()); + EXPECT_DOUBLE_EQ( + 0, estimator.GetEstimatedPosition().Rotation().Radians().value()); + // Test reset rotation estimator.ResetRotation(wpi::math::Rotation2d{90_deg}); - EXPECT_DOUBLE_EQ(2, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); EXPECT_DOUBLE_EQ(0, estimator.GetEstimatedPosition().Y().value()); EXPECT_DOUBLE_EQ( std::numbers::pi / 2, @@ -481,19 +491,30 @@ TEST(SwerveDrivePoseEstimatorTest, TestReset) { modulePosition, modulePosition}); } - EXPECT_DOUBLE_EQ(2, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); EXPECT_DOUBLE_EQ(1, estimator.GetEstimatedPosition().Y().value()); EXPECT_DOUBLE_EQ( std::numbers::pi / 2, estimator.GetEstimatedPosition().Rotation().Radians().value()); + // Add a vision measurement with a different rotation + estimator.AddVisionMeasurement( + wpi::math::Pose2d{2.5_m, 1_m, wpi::math::Rotation2d{180_deg}}, + wpi::math::MathSharedStore::GetTimestamp()); + + EXPECT_DOUBLE_EQ(2.5, estimator.GetEstimatedPosition().X().value()); + EXPECT_DOUBLE_EQ(1, estimator.GetEstimatedPosition().Y().value()); + EXPECT_DOUBLE_EQ( + std::numbers::pi * 3.0 / 4, + estimator.GetEstimatedPosition().Rotation().Radians().value()); + // Test reset translation estimator.ResetTranslation(wpi::math::Translation2d{-1_m, -1_m}); EXPECT_DOUBLE_EQ(-1, estimator.GetEstimatedPosition().X().value()); EXPECT_DOUBLE_EQ(-1, estimator.GetEstimatedPosition().Y().value()); EXPECT_DOUBLE_EQ( - std::numbers::pi / 2, + std::numbers::pi * 3.0 / 4, estimator.GetEstimatedPosition().Rotation().Radians().value()); // Test reset pose diff --git a/wpimath/src/test/native/cpp/filter/EdgeCounterFilterTest.cpp b/wpimath/src/test/native/cpp/filter/EdgeCounterFilterTest.cpp new file mode 100644 index 0000000000..5aabe6a67c --- /dev/null +++ b/wpimath/src/test/native/cpp/filter/EdgeCounterFilterTest.cpp @@ -0,0 +1,77 @@ +// 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 "wpi/math/filter/EdgeCounterFilter.hpp" + +#include + +#include "wpi/units/time.hpp" +#include "wpi/util/timestamp.h" + +static wpi::units::second_t now = 0_s; + +class EdgeCounterFilterTest : public ::testing::Test { + protected: + void SetUp() override { + WPI_SetNowImpl( + [] { return wpi::units::microsecond_t{now}.to(); }); + now = 0_ms; + } + + void TearDown() override { + now = 0_ms; + WPI_SetNowImpl(nullptr); + } +}; + +TEST_F(EdgeCounterFilterTest, EdgeCounterFilterActivated) { + wpi::math::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) { + wpi::math::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) { + wpi::math::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); +} diff --git a/wpimath/src/test/native/cpp/geometry/Rotation2dTest.cpp b/wpimath/src/test/native/cpp/geometry/Rotation2dTest.cpp index 1cedf1ad50..0792c7e1e0 100644 --- a/wpimath/src/test/native/cpp/geometry/Rotation2dTest.cpp +++ b/wpimath/src/test/native/cpp/geometry/Rotation2dTest.cpp @@ -41,6 +41,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}; diff --git a/wpimath/src/test/native/cpp/geometry/Rotation3dTest.cpp b/wpimath/src/test/native/cpp/geometry/Rotation3dTest.cpp index f8bc7f4c4d..0b067dd33c 100644 --- a/wpimath/src/test/native/cpp/geometry/Rotation3dTest.cpp +++ b/wpimath/src/test/native/cpp/geometry/Rotation3dTest.cpp @@ -261,6 +261,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}; @@ -379,4 +394,32 @@ TEST(Rotation3dTest, Interpolate) { EXPECT_DOUBLE_EQ(0.0, wpi::units::degree_t{interpolated.X()}.value()); EXPECT_DOUBLE_EQ(0.0, wpi::units::degree_t{interpolated.Y()}.value()); EXPECT_DOUBLE_EQ(-175.0, wpi::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::util::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::util::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::util::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); } diff --git a/wpimath/src/test/native/cpp/kinematics/DifferentialDriveOdometry3dTest.cpp b/wpimath/src/test/native/cpp/kinematics/DifferentialDriveOdometry3dTest.cpp index 14e3d62293..74b3310775 100644 --- a/wpimath/src/test/native/cpp/kinematics/DifferentialDriveOdometry3dTest.cpp +++ b/wpimath/src/test/native/cpp/kinematics/DifferentialDriveOdometry3dTest.cpp @@ -8,10 +8,10 @@ #include -static constexpr double kEpsilon = 1E-9; - using namespace wpi::math; +static constexpr double kEpsilon = 1E-9; + TEST(DifferentialDriveOdometry3dTest, Initialize) { DifferentialDriveOdometry3d odometry{ wpi::math::Rotation3d{0_deg, 0_deg, 90_deg}, 0_m, 0_m, @@ -39,3 +39,20 @@ 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{ + wpi::math::Rotation3d{0_deg, 5_deg, 0_deg}, 0_m, 0_m, + wpi::math::Pose3d{wpi::math::Translation3d{}, + wpi::math::Rotation3d{0_deg, 0_deg, 90_deg}}}; + const auto& pose = + odometry.Update(wpi::math::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(wpi::units::degree_t{pose.Rotation().X()}.value(), 0.0, kEpsilon); + EXPECT_NEAR(wpi::units::degree_t{pose.Rotation().Y()}.value(), 5.0, kEpsilon); + EXPECT_NEAR(wpi::units::degree_t{pose.Rotation().Z()}.value(), 90.0, + kEpsilon); +} diff --git a/wpimath/src/test/native/cpp/kinematics/MecanumDriveOdometry3dTest.cpp b/wpimath/src/test/native/cpp/kinematics/MecanumDriveOdometry3dTest.cpp index 173a6d84df..bb397baf8c 100644 --- a/wpimath/src/test/native/cpp/kinematics/MecanumDriveOdometry3dTest.cpp +++ b/wpimath/src/test/native/cpp/kinematics/MecanumDriveOdometry3dTest.cpp @@ -231,3 +231,20 @@ TEST_F(MecanumDriveOdometry3dTest, AccuracyFacingXAxis) { EXPECT_LT(errorSum / (trajectory.TotalTime().value() / dt.value()), 0.06); EXPECT_LT(maxError, 0.125); } + +TEST_F(MecanumDriveOdometry3dTest, GyroOffset) { + wpi::math::MecanumDriveWheelPositions wheelPositions; + odometry.ResetPosition( + wpi::math::Rotation3d{0_deg, 5_deg, 0_deg}, wheelPositions, + wpi::math::Pose3d{wpi::math::Translation3d{}, + wpi::math::Rotation3d{0_deg, 0_deg, 90_deg}}); + auto pose = odometry.Update(wpi::math::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(wpi::units::degree_t{pose.Rotation().X()}.value(), 0.0, 1e-9); + EXPECT_NEAR(wpi::units::degree_t{pose.Rotation().Y()}.value(), 5.0, 1e-9); + EXPECT_NEAR(wpi::units::degree_t{pose.Rotation().Z()}.value(), 90.0, 1e-9); +} diff --git a/wpimath/src/test/native/cpp/kinematics/SwerveDriveOdometry3dTest.cpp b/wpimath/src/test/native/cpp/kinematics/SwerveDriveOdometry3dTest.cpp index e9c473305e..dd721d90ca 100644 --- a/wpimath/src/test/native/cpp/kinematics/SwerveDriveOdometry3dTest.cpp +++ b/wpimath/src/test/native/cpp/kinematics/SwerveDriveOdometry3dTest.cpp @@ -225,3 +225,19 @@ 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( + wpi::math::Rotation3d{0_deg, 5_deg, 0_deg}, {zero, zero, zero, zero}, + wpi::math::Pose3d{wpi::math::Translation3d{}, + wpi::math::Rotation3d{0_deg, 0_deg, 90_deg}}); + auto pose = m_odometry.Update(wpi::math::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(wpi::units::degree_t{pose.Rotation().X()}.value(), 0.0, 1e-9); + EXPECT_NEAR(wpi::units::degree_t{pose.Rotation().Y()}.value(), 5.0, 1e-9); + EXPECT_NEAR(wpi::units::degree_t{pose.Rotation().Z()}.value(), 90.0, 1e-9); +} diff --git a/wpiunits/src/main/java/org/wpilib/units/Units.java b/wpiunits/src/main/java/org/wpilib/units/Units.java index 0acf2b3e1c..9b1e41cbea 100644 --- a/wpiunits/src/main/java/org/wpilib/units/Units.java +++ b/wpiunits/src/main/java/org/wpilib/units/Units.java @@ -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 diff --git a/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp b/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp index 58e6a1e4a7..4c97723d08 100644 --- a/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp +++ b/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp @@ -189,7 +189,7 @@ class MemoryBufferMMapFile : public MB { MappedFileRegion m_mfr; static uint64_t getLegalMapOffset(uint64_t offset) { - return offset & ~(MappedFileRegion::GetAlignment() - 1); + return offset & ~(static_cast(MappedFileRegion::GetAlignment()) - 1); } static uint64_t getLegalMapSize(uint64_t len, uint64_t offset) {