From 5cf55bd9949de3ba3d5b80f974be17d2ad72cea7 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 28 Jan 2026 16:26:54 -0500 Subject: [PATCH 01/19] Add initial turret subsystem implementation with CAN IDs and sensor inputs --- .idea/copilot.data.migration.edit.xml | 6 + src/main/deploy/pathplanner/navgrid.json | 1 - src/main/deploy/vision/FRC2026_WELDED.fmap | 1 + src/main/java/frc/lib/FieldConstants.java | 353 ++++++++++++++++++ src/main/java/frc/robot/CanID.java | 2 + .../drive/CommandSwerveDrivetrain.java | 66 ++-- .../frc/robot/subsystems/turret/Turret.java | 43 +++ .../frc/robot/subsystems/turret/TurretIO.java | 14 + .../turret/TurretIOSensorInputs.java | 45 +++ .../subsystems/vision/photon/Camera.java | 7 +- 10 files changed, 501 insertions(+), 37 deletions(-) create mode 100644 .idea/copilot.data.migration.edit.xml delete mode 100644 src/main/deploy/pathplanner/navgrid.json create mode 100644 src/main/deploy/vision/FRC2026_WELDED.fmap create mode 100644 src/main/java/frc/lib/FieldConstants.java create mode 100644 src/main/java/frc/robot/subsystems/turret/Turret.java create mode 100644 src/main/java/frc/robot/subsystems/turret/TurretIO.java create mode 100644 src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/main/deploy/pathplanner/navgrid.json b/src/main/deploy/pathplanner/navgrid.json deleted file mode 100644 index ac5f521..0000000 --- a/src/main/deploy/pathplanner/navgrid.json +++ /dev/null @@ -1 +0,0 @@ -{"field_size":{"x":16.54,"y":8.07},"nodeSizeMeters":0.3,"grid":[[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,true,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,true,true,true,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]]} \ No newline at end of file diff --git a/src/main/deploy/vision/FRC2026_WELDED.fmap b/src/main/deploy/vision/FRC2026_WELDED.fmap new file mode 100644 index 0000000..6fa3319 --- /dev/null +++ b/src/main/deploy/vision/FRC2026_WELDED.fmap @@ -0,0 +1 @@ +{"fiducials":[{"family":"apriltag3_36h11_classic","id":1,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.6074798,1.2246467991473532e-16,-1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":2,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,3.644919399999999,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":3,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.0413645999999996,1.2246467991473532e-16,-1,0,0.35573759999999943,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":4,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.0413645999999996,1.2246467991473532e-16,-1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":5,"size":165.1,"transform":[-2.220446049250313e-16,1,0,3.644919399999999,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":6,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.6074798,1.2246467991473532e-16,-1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":7,"size":165.1,"transform":[1,0,0,3.6823844,0,1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":8,"size":165.1,"transform":[-2.220446049250313e-16,1,0,4.0005194,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":9,"size":165.1,"transform":[1,0,0,4.248677399999998,0,1,0,-0.35546240000000084,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":10,"size":165.1,"transform":[1,0,0,4.248677399999998,0,1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":11,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,4.0005194,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":12,"size":165.1,"transform":[1,0,0,3.6823844,0,1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":13,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.262817199999999,1.2246467991473532e-16,-1,0,3.368812599999999,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":14,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.262817199999999,1.2246467991473532e-16,-1,0,2.937012599999999,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":15,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.2624616,1.2246467991473532e-16,-1,0,0.2890625999999994,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":16,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.2624616,1.2246467991473532e-16,-1,0,-0.14273740000000057,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":17,"size":165.1,"transform":[1,0,0,-3.6074156000000004,0,1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":18,"size":165.1,"transform":[-2.220446049250313e-16,1,0,-3.6448806000000005,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":19,"size":165.1,"transform":[1,0,0,-3.041325800000001,0,1,0,-0.35546240000000084,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":20,"size":165.1,"transform":[1,0,0,-3.041325800000001,0,1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":21,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,-3.6448806000000005,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":22,"size":165.1,"transform":[1,0,0,-3.6074156000000004,0,1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":23,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-3.6823202000000004,1.2246467991473532e-16,-1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":24,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,-4.0004806,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":25,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-4.2486386000000005,1.2246467991473532e-16,-1,0,0.35573759999999943,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":26,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-4.2486386000000005,1.2246467991473532e-16,-1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":27,"size":165.1,"transform":[-2.220446049250313e-16,1,0,-4.0004806,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":28,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-3.6823202000000004,1.2246467991473532e-16,-1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":29,"size":165.1,"transform":[1,0,0,-8.262753,0,1,0,-3.3685374000000006,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":30,"size":165.1,"transform":[1,0,0,-8.262753,0,1,0,-2.9367374,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":31,"size":165.1,"transform":[1,0,0,-8.2624228,0,1,0,-0.2887874000000008,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":32,"size":165.1,"transform":[1,0,0,-8.2624228,0,1,0,0.1430125999999996,0,0,1,0.55245,0,0,0,1],"unique":true}],"type":"frc","fieldlength":16.541,"fieldwidth":8.069,"pngBase64":"data:image/png;base64,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Uk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAMAUExURQAAAP///2VlZWZmZmpqamlpaWhoaGdnZwsGCNrV1y4aLp6KojoqPuro6wICtwICsQMD1gMDyQICpQICkQICbAQEvwkJ7QoK9gkJ4wcHrwQEUhQU7BYW9Q0NlxISxxERuBUV1xYWcCIikT4+/Tc33kVF/hcXPV9fiklJamdnk3FxoFxcfJKSvH9/pLS04AwMDRYWFzExMgMEKAIDEyIjMh0eKnJ0gl1falRVWzM1P1laXywwPigrNjk8R0FEThEVIRgbI09RV4SHjwQFB0dMUwkQGHuBiDU4O2ltcZaZm3R9ggUKDIGGiGJqbXJ2d1pfYCAnKAwrKyIxMTRJSS4/P0ZaWltwcFdoaFFgYGp7e2Rzc5uoqIqOjpKWlklKSpainyImIjpuMlKKRlZ2TgYKAgsLBGFhS1VVRWVlVltbT2xsX3R0a2BgWYeHf2ZmYI6OiIqKhW5uanp6dpKSjpaWkv78kE1LOfzqcxMRBoGAehkWBqSVTyklFTEuITYzJyIdC/HRXDMtGEI9KiknIOK9SbqdP6iOOpiCOHxpL414N7CMIJh5IYtvH7GMKaCAJbqULaiHKWtWGn1kH2FOGEE0EMOcMXRdHVZFFpd6J4RrI7COL45zJks9FJ6BK8ukONWuP2NTJlNGIT41GkY8H1pZVi4kDDcsDzs5NBwZE0dANQcEAiUbFSYfGzsyLTMiIb4CArYCArECAqUCApQCAtMEBHcDAzoCAvcJCVEEBOYMDM0NDbwMDB8CAqwMDJ4MDOwUFPwWFhICAq8cHP0qKsIiIuYqKv01Na0lJYseHlgVFSoNDUAYGHhTU4dfX2dJSZFoaFlAQJxycriQkKWBgXdhYd62toBqamVVVVFGRoB6em9qapyWlmpmZnp2doqGhoaCgpaSkrOxscHBwaWlpZubm5aWlpKSko6OjoqKioaGhoKCgn5+fnp6enZ2dnJycm5ubmJiYl5eXlpaWlZWVlJSUk5OTkNDQzs7OzY2NisrKyYmJiIiIhwcHBISEgICAv////17gRMAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAAsTAAALEwEAmpwYAAC6uUlEQVR4nNR9d4AU9dn/55mZnS2z5doeTSkxhpJiN2JMFBEEC5jYYo0NBDVG42Hhou8vMXdGPTQaFQU0JjFFkxiBKIhiefNG7OVNXpAYBRQO7vZuy+1tnfL8/piZLXe7e7tHkTwHd7tTvt/vzDzzPJ/v074kCWAGQtgNCpp/QgWfc9+rOLvouGD+zNv7X0h/4dDbqXgvCVd86+aUS7/rf1aAjYG9MADCQOLBvVLFvYMOLXEM55sxFE1AUqzQUK673DFtuX2Lrb/taLX2LB54cGEr7dYpXOIQAqi9lb9/1ggy5HvvF5nBQMJj77+r8zq165FfCea3pLU9ASWhAEjAA6Dl7oGD7wCAGwZd000AgJ8N2l5MXHivK99q4onnUioQYgANQzRbmcJAYRvm12G2Gc6d94Pkn7MjT1puWF/ZbJgQvPkhDWLn9XcADDQUMSATJGgF1z2YOwEAkma1SQAgFu7Kn0wAaZCgARDBFe5nYpejITGiYMPglwwg8/xB72muVWIQwDSoHy68Fvv9sA9qs9kX7a3mmPmas6gOmUeXisxA2+J8e8b3b4jW/fznVP51NJky/w2AwsRUfFgLgA4M3m7ybgnmzdFNt99akoVvvv3WOwHjSxIMDGx0t6kB4eEzuHWm9VQCA3cTsGA1AEAyNxR3RRIAKc+RVHBkEUkANJC5r+gG5G8HE1g0n53ElfgxBMHsIxREKBgKhoLFu80NA9ixkA/z/aHocdicyNZPfhvbPNXWmuOLxfkTvfm2C/gRDKgCQFzAkcQFY0goRRepgImR9GAA53UAYFp098DtaLH2l2XJO7MjSm2+1xpLr1DifaydGhpQLBB3T+CaRLcBiJXYce3pAAANpeSfKfns7WX50SKpwj4AzNZRlbQxAAYSDABBIIggggP2h4KhkMmJheKR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"} \ No newline at end of file diff --git a/src/main/java/frc/lib/FieldConstants.java b/src/main/java/frc/lib/FieldConstants.java new file mode 100644 index 0000000..a0e321d --- /dev/null +++ b/src/main/java/frc/lib/FieldConstants.java @@ -0,0 +1,353 @@ +// Copyright (c) 2025-2026 Littleton Robotics +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package frc.lib; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.Filesystem; +import java.io.IOException; + +/** + * Contains information for location of field element and other useful reference points. + * + *

NOTE: All constants are defined relative to the field coordinate system, and from the + * perspective of the blue alliance station + */ +public class FieldConstants { + public static final FieldType fieldType = FieldType.WELDED; + + // AprilTag related constants + public static final int aprilTagCount = AprilTagLayoutType.OFFICIAL.getLayout().getTags().size(); + public static final double aprilTagWidth = Units.inchesToMeters(6.5); + public static final AprilTagLayoutType defaultAprilTagType = AprilTagLayoutType.OFFICIAL; + + // Field dimensions + public static final double fieldLength = AprilTagLayoutType.OFFICIAL.getLayout().getFieldLength(); + public static final double fieldWidth = AprilTagLayoutType.OFFICIAL.getLayout().getFieldWidth(); + + /** + * Officially defined and relevant vertical lines found on the field (defined by X-axis offset) + */ + public static class LinesVertical { + public static final double center = fieldLength / 2.0; + public static final double starting = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(26).get().getX(); + public static final double allianceZone = starting; + public static final double hubCenter = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(26).get().getX() + Hub.width / 2.0; + public static final double neutralZoneNear = center - Units.inchesToMeters(120); + public static final double neutralZoneFar = center + Units.inchesToMeters(120); + public static final double oppHubCenter = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(4).get().getX() + Hub.width / 2.0; + public static final double oppAllianceZone = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(10).get().getX(); + } + + /** + * Officially defined and relevant horizontal lines found on the field (defined by Y-axis offset) + * + *

NOTE: The field element start and end are always left to right from the perspective of the + * alliance station + */ + public static class LinesHorizontal { + + public static final double center = fieldWidth / 2.0; + + // Right of hub + public static final double rightBumpStart = Hub.nearRightCorner.getY(); + public static final double rightBumpEnd = rightBumpStart - RightBump.width; + public static final double rightTrenchOpenStart = rightBumpEnd - Units.inchesToMeters(12.0); + public static final double rightTrenchOpenEnd = 0; + + // Left of hub + public static final double leftBumpEnd = Hub.nearLeftCorner.getY(); + public static final double leftBumpStart = leftBumpEnd + LeftBump.width; + public static final double leftTrenchOpenEnd = leftBumpStart + Units.inchesToMeters(12.0); + public static final double leftTrenchOpenStart = fieldWidth; + } + + /** Hub related constants */ + public static class Hub { + + // Dimensions + public static final double width = Units.inchesToMeters(47.0); + public static final double height = + Units.inchesToMeters(72.0); // includes the catcher at the top + public static final double innerWidth = Units.inchesToMeters(41.7); + public static final double innerHeight = Units.inchesToMeters(56.5); + + // Relevant reference points on alliance side + public static final Translation3d topCenterPoint = + new Translation3d( + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(26).get().getX() + width / 2.0, + fieldWidth / 2.0, + height); + public static final Translation3d innerCenterPoint = + new Translation3d( + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(26).get().getX() + width / 2.0, + fieldWidth / 2.0, + innerHeight); + + public static final Translation2d nearLeftCorner = + new Translation2d(topCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d nearRightCorner = + new Translation2d(topCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 - width / 2.0); + public static final Translation2d farLeftCorner = + new Translation2d(topCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d farRightCorner = + new Translation2d(topCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 - width / 2.0); + + // Relevant reference points on the opposite side + public static final Translation3d oppTopCenterPoint = + new Translation3d( + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(4).get().getX() + width / 2.0, + fieldWidth / 2.0, + height); + public static final Translation2d oppNearLeftCorner = + new Translation2d(oppTopCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d oppNearRightCorner = + new Translation2d(oppTopCenterPoint.getX() - width / 2.0, fieldWidth / 2.0 - width / 2.0); + public static final Translation2d oppFarLeftCorner = + new Translation2d(oppTopCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 + width / 2.0); + public static final Translation2d oppFarRightCorner = + new Translation2d(oppTopCenterPoint.getX() + width / 2.0, fieldWidth / 2.0 - width / 2.0); + + // Hub faces + public static final Pose2d nearFace = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(26).get().toPose2d(); + public static final Pose2d farFace = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(20).get().toPose2d(); + public static final Pose2d rightFace = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(18).get().toPose2d(); + public static final Pose2d leftFace = + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(21).get().toPose2d(); + } + + /** Left Bump related constants */ + public static class LeftBump { + + // Dimensions + public static final double width = Units.inchesToMeters(73.0); + public static final double height = Units.inchesToMeters(6.513); + public static final double depth = Units.inchesToMeters(44.4); + + // Relevant reference points on alliance side + public static final Translation2d nearLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d nearRightCorner = Hub.nearLeftCorner; + public static final Translation2d farLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d farRightCorner = Hub.farLeftCorner; + + // Relevant reference points on opposing side + public static final Translation2d oppNearLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppNearRightCorner = Hub.oppNearLeftCorner; + public static final Translation2d oppFarLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppFarRightCorner = Hub.oppFarLeftCorner; + } + + /** Right Bump related constants */ + public static class RightBump { + // Dimensions + public static final double width = Units.inchesToMeters(73.0); + public static final double height = Units.inchesToMeters(6.513); + public static final double depth = Units.inchesToMeters(44.4); + + // Relevant reference points on alliance side + public static final Translation2d nearLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d nearRightCorner = Hub.nearLeftCorner; + public static final Translation2d farLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d farRightCorner = Hub.farLeftCorner; + + // Relevant reference points on opposing side + public static final Translation2d oppNearLeftCorner = + new Translation2d(LinesVertical.hubCenter + width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppNearRightCorner = Hub.oppNearLeftCorner; + public static final Translation2d oppFarLeftCorner = + new Translation2d(LinesVertical.hubCenter - width / 2, Units.inchesToMeters(255)); + public static final Translation2d oppFarRightCorner = Hub.oppFarLeftCorner; + } + + /** Left Trench related constants */ + public static class LeftTrench { + // Dimensions + public static final double width = Units.inchesToMeters(65.65); + public static final double depth = Units.inchesToMeters(47.0); + public static final double height = Units.inchesToMeters(40.25); + public static final double openingWidth = Units.inchesToMeters(50.34); + public static final double openingHeight = Units.inchesToMeters(22.25); + + // Relevant reference points on alliance side + public static final Translation3d openingTopLeft = + new Translation3d(LinesVertical.hubCenter, fieldWidth, openingHeight); + public static final Translation3d openingTopRight = + new Translation3d(LinesVertical.hubCenter, fieldWidth - openingWidth, openingHeight); + + // Relevant reference points on opposing side + public static final Translation3d oppOpeningTopLeft = + new Translation3d(LinesVertical.oppHubCenter, fieldWidth, openingHeight); + public static final Translation3d oppOpeningTopRight = + new Translation3d(LinesVertical.oppHubCenter, fieldWidth - openingWidth, openingHeight); + } + + public static class RightTrench { + + // Dimensions + public static final double width = Units.inchesToMeters(65.65); + public static final double depth = Units.inchesToMeters(47.0); + public static final double height = Units.inchesToMeters(40.25); + public static final double openingWidth = Units.inchesToMeters(50.34); + public static final double openingHeight = Units.inchesToMeters(22.25); + + // Relevant reference points on alliance side + public static final Translation3d openingTopLeft = + new Translation3d(LinesVertical.hubCenter, openingWidth, openingHeight); + public static final Translation3d openingTopRight = + new Translation3d(LinesVertical.hubCenter, 0, openingHeight); + + // Relevant reference points on opposing side + public static final Translation3d oppOpeningTopLeft = + new Translation3d(LinesVertical.oppHubCenter, openingWidth, openingHeight); + public static final Translation3d oppOpeningTopRight = + new Translation3d(LinesVertical.oppHubCenter, 0, openingHeight); + } + + /** Tower related constants */ + public static class Tower { + // Dimensions + public static final double width = Units.inchesToMeters(49.25); + public static final double depth = Units.inchesToMeters(45.0); + public static final double height = Units.inchesToMeters(78.25); + public static final double innerOpeningWidth = Units.inchesToMeters(32.250); + public static final double frontFaceX = Units.inchesToMeters(43.51); + + public static final double uprightHeight = Units.inchesToMeters(72.1); + + // Rung heights from the floor + public static final double lowRungHeight = Units.inchesToMeters(27.0); + public static final double midRungHeight = Units.inchesToMeters(45.0); + public static final double highRungHeight = Units.inchesToMeters(63.0); + + // Relevant reference points on alliance side + public static final Translation2d centerPoint = + new Translation2d( + frontFaceX, AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(31).get().getY()); + public static final Translation2d leftUpright = + new Translation2d( + frontFaceX, + (AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(31).get().getY()) + + innerOpeningWidth / 2 + + Units.inchesToMeters(0.75)); + public static final Translation2d rightUpright = + new Translation2d( + frontFaceX, + (AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(31).get().getY()) + - innerOpeningWidth / 2 + - Units.inchesToMeters(0.75)); + + // Relevant reference points on opposing side + public static final Translation2d oppCenterPoint = + new Translation2d( + fieldLength - frontFaceX, + AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(15).get().getY()); + public static final Translation2d oppLeftUpright = + new Translation2d( + fieldLength - frontFaceX, + (AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(15).get().getY()) + + innerOpeningWidth / 2 + + Units.inchesToMeters(0.75)); + public static final Translation2d oppRightUpright = + new Translation2d( + fieldLength - frontFaceX, + (AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(15).get().getY()) + - innerOpeningWidth / 2 + - Units.inchesToMeters(0.75)); + } + + public static class Depot { + // Dimensions + public static final double width = Units.inchesToMeters(42.0); + public static final double depth = Units.inchesToMeters(27.0); + public static final double height = Units.inchesToMeters(1.125); + public static final double distanceFromCenterY = Units.inchesToMeters(75.93); + + // Relevant reference points on alliance side + public static final Translation3d depotCenter = + new Translation3d(depth, (fieldWidth / 2) + distanceFromCenterY, height); + public static final Translation3d leftCorner = + new Translation3d(depth, (fieldWidth / 2) + distanceFromCenterY + (width / 2), height); + public static final Translation3d rightCorner = + new Translation3d(depth, (fieldWidth / 2) + distanceFromCenterY - (width / 2), height); + } + + public static class Outpost { + // Dimensions + public static final double width = Units.inchesToMeters(31.8); + public static final double openingDistanceFromFloor = Units.inchesToMeters(28.1); + public static final double height = Units.inchesToMeters(7.0); + + // Relevant reference points on alliance side + public static final Translation2d centerPoint = + new Translation2d(0, AprilTagLayoutType.OFFICIAL.getLayout().getTagPose(29).get().getY()); + } + + public enum FieldType { + ANDYMARK("andymark"), + WELDED("welded"); + + private final String name; + + FieldType(String name) { + this.name = name; + } + } + + public enum AprilTagLayoutType { + OFFICIAL("2026-official"), + NONE("2026-none"); + + private final String name; + private volatile AprilTagFieldLayout layout; + private volatile String layoutString; + + AprilTagLayoutType(String name) { + this.name = name; + } + + public AprilTagFieldLayout getLayout() { + if (layout == null) { + synchronized (this) { + if (layout == null) { + try { + layout = AprilTagFieldLayout.loadFromResource(Filesystem.getDeployDirectory().toPath().resolve("FRC2026_WELDED.fmap").toString()); + layoutString = new ObjectMapper().writeValueAsString(layout); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + return layout; + } + + public String getLayoutString() { + if (layoutString == null) { + getLayout(); + } + return layoutString; + } + } +} \ No newline at end of file diff --git a/src/main/java/frc/robot/CanID.java b/src/main/java/frc/robot/CanID.java index ec46871..2d4b3c6 100644 --- a/src/main/java/frc/robot/CanID.java +++ b/src/main/java/frc/robot/CanID.java @@ -4,6 +4,8 @@ * Holder for all CAN device IDs besides drivetrain devices */ public enum CanID { + TURRET_ENCODER(11), + TURRET_MOTOR(12) ; private int deviceID; diff --git a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java index cf3933f..1d699e5 100644 --- a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java +++ b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java @@ -302,39 +302,39 @@ public void addVisionMeasurement( } private void configureAutoBuilder() { - try { - RobotConfig config = RobotConfig.fromGUISettings(); - AutoBuilder.configure( - () -> getState().Pose, - this::resetPose, - () -> getState().Speeds, - (speeds, feedForwards) -> setControl( - m_pathApplyRobotSpeeds.withSpeeds(speeds) - .withWheelForceFeedforwardsX(feedForwards.robotRelativeForcesXNewtons()) - .withWheelForceFeedforwardsY(feedForwards.robotRelativeForcesYNewtons()) - ), - new PPHolonomicDriveController( - new PIDConstants(5.0, 0, 0, 0), - new PIDConstants(5.0, 0, 0, 0) - ), - config, - () -> false, - this - ); - Pathfinding.setPathfinder(new LocalADStarAK()); - - PathPlannerLogging.setLogActivePathCallback( - (activePath) -> { - Logger.recordOutput( - "Odometry/Trajectory", activePath.toArray(new Pose2d[0])); - }); - PathPlannerLogging.setLogTargetPoseCallback( - (targetPose) -> { - Logger.recordOutput("Odometry/TrajectorySetpoint", targetPose); - }); - } catch (Exception e) { - DriverStation.reportError("Failed to load PathPlanner config and configure AutoBuilder", e.getStackTrace()); - } +// try { +// RobotConfig config = RobotConfig.fromGUISettings(); +// AutoBuilder.configure( +// () -> getState().Pose, +// this::resetPose, +// () -> getState().Speeds, +// (speeds, feedForwards) -> setControl( +// m_pathApplyRobotSpeeds.withSpeeds(speeds) +// .withWheelForceFeedforwardsX(feedForwards.robotRelativeForcesXNewtons()) +// .withWheelForceFeedforwardsY(feedForwards.robotRelativeForcesYNewtons()) +// ), +// new PPHolonomicDriveController( +// new PIDConstants(5.0, 0, 0, 0), +// new PIDConstants(5.0, 0, 0, 0) +// ), +// config, +// () -> false, +// this +// ); +// Pathfinding.setPathfinder(new LocalADStarAK()); +// +// PathPlannerLogging.setLogActivePathCallback( +// (activePath) -> { +// Logger.recordOutput( +// "Odometry/Trajectory", activePath.toArray(new Pose2d[0])); +// }); +// PathPlannerLogging.setLogTargetPoseCallback( +// (targetPose) -> { +// Logger.recordOutput("Odometry/TrajectorySetpoint", targetPose); +// }); +// } catch (Exception e) { +// DriverStation.reportError("Failed to load PathPlanner config and configure AutoBuilder", e.getStackTrace()); +// } } diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java new file mode 100644 index 0000000..7ddf8ee --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -0,0 +1,43 @@ +package frc.robot.subsystems.turret; + +import edu.wpi.first.math.geometry.Translation2d; +import frc.lib.FieldConstants; +import frc.lib.subsystem.SpikeSystem; +import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import org.littletonrobotics.junction.Logger; + +public class Turret extends SpikeSystem { + private final TurretIO.TurretIOInputs inputs = new TurretIO.TurretIOInputs(); + private final TurretIOSensorInputs sensorData; + private final CommandSwerveDrivetrain drive; + + public Turret(CommandSwerveDrivetrain drive) { + super("Turret"); + this.drive = drive; + this.sensorData = new TurretIOSensorInputs(null); // TODO: add camera + } + + @Override + public void onPeriodic() { + Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); + } + + @Override + protected Runnable setupDataRefresher() { + return useAsyncDataRefresher(inputs, sensorData); + } + + public double getAngleOffsetFromHub() { + Translation2d hubLocation = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + // get robot position + Translation2d robotPosition = drive.getPose().getTranslation(); + // calculate angle difference between turret and hub + double angleToHub = Math.toDegrees(Math.atan2(hubLocation.getY() - robotPosition.getY(), + hubLocation.getX() - robotPosition.getX())); + double turretAngle = inputs.turretAngleDegrees; + double angleOffset = angleToHub - turretAngle; + // normalize angle to [-180, 180] + angleOffset = ((angleOffset + 180) % 360) - 180; + return angleOffset; + } +} diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIO.java b/src/main/java/frc/robot/subsystems/turret/TurretIO.java new file mode 100644 index 0000000..6a23472 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/TurretIO.java @@ -0,0 +1,14 @@ +package frc.robot.subsystems.turret; + +import frc.lib.subsystem.BaseIO; +import frc.lib.subsystem.BaseInputClass; +import org.littletonrobotics.junction.AutoLog; + +public interface TurretIO extends BaseIO { + + @AutoLog + public static class TurretIOInputs extends BaseInputClass { + public double turretAngleDegrees = 0.0; + public double offsetError = 0.0; // feed from camera, raw error value + } +} diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java new file mode 100644 index 0000000..2827884 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -0,0 +1,45 @@ +package frc.robot.subsystems.turret; + +import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.hardware.CANcoder; +import com.ctre.phoenix6.hardware.TalonFX; +import edu.wpi.first.units.Units; +import frc.lib.subsystem.IORefresher; +import frc.robot.CanID; +import frc.robot.subsystems.vision.photon.Camera; + +public class TurretIOSensorInputs implements TurretIO, IORefresher { + private final CANcoder turretEncoder; + private final Camera turretCamera; + private final TalonFX turretMotor; + + private final MotionMagicVoltage mmRequest = new MotionMagicVoltage(0.0); + + private static final double kTurretGearRatio = 100.0; // 100:1, this isn't the real value + + private static final double kTurretMinAngleDegrees = -180.0; + private static final double kTurretMaxAngleDegrees = 180.0; + + public TurretIOSensorInputs(Camera camera) { + this.turretEncoder = new CANcoder(CanID.TURRET_ENCODER.getID()); + this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); + this.turretCamera = camera; + } + + public void setTurretAngle(double angle) { + double clamped = Math.max(kTurretMinAngleDegrees, Math.min(kTurretMaxAngleDegrees, angle)); + double motorRotations = (clamped / 360.0) * kTurretGearRatio; + + turretMotor.setControl(mmRequest.withPosition(motorRotations)); + } + + @Override + public void updateInputs(TurretIOInputs inputs) { + inputs.turretAngleDegrees = turretEncoder.getPosition().getValue().in(Units.Degree); + } + + @Override + public void refreshData() { + + } +} diff --git a/src/main/java/frc/robot/subsystems/vision/photon/Camera.java b/src/main/java/frc/robot/subsystems/vision/photon/Camera.java index efa1dab..2a1ba86 100644 --- a/src/main/java/frc/robot/subsystems/vision/photon/Camera.java +++ b/src/main/java/frc/robot/subsystems/vision/photon/Camera.java @@ -1,13 +1,14 @@ package frc.robot.subsystems.vision.photon; import edu.wpi.first.apriltag.AprilTagFieldLayout; -import edu.wpi.first.apriltag.AprilTagFields; import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.wpilibj.Filesystem; import org.photonvision.EstimatedRobotPose; import org.photonvision.PhotonCamera; import org.photonvision.PhotonPoseEstimator; import org.photonvision.targeting.PhotonPipelineResult; +import java.io.IOException; import java.util.List; import java.util.Optional; @@ -17,12 +18,12 @@ public class Camera { private transient final PhotonPoseEstimator poseEstimator; private final Transform3d cameraToRobot; - public Camera(String id, Transform3d cameraToRobot) { + public Camera(String id, Transform3d cameraToRobot) throws IOException { this.cameraToRobot = cameraToRobot; this.id = id; this.photonCamera = new PhotonCamera(id); this.poseEstimator = new PhotonPoseEstimator( - AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField), + AprilTagFieldLayout.loadFromResource(Filesystem.getDeployDirectory().toPath().resolve("FRC2026_WELDED.fmap").toString()), PhotonPoseEstimator.PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, cameraToRobot ); From 662527b2a038a8bdd6a72d75ea4f4d342fbdc779 Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 29 Jan 2026 16:06:53 -0500 Subject: [PATCH 02/19] Add initial turret subsystem implementation with CAN IDs and sensor inputs --- .idea/compiler.xml | 4 +- .idea/misc.xml | 2 +- build.gradle | 2 +- src/main/java/frc/lib/FieldConstants.java | 3 +- .../java/frc/lib/SubsystemDataProcessor.java | 1 - src/main/java/frc/lib/state/StateMachine.java | 122 +++++++++++ .../java/frc/lib/subsystem/SpikeSystem.java | 1 - src/main/java/frc/robot/BuildConstants.java | 17 ++ src/main/java/frc/robot/Constants.java | 37 ++++ src/main/java/frc/robot/Robot.java | 200 +++++++++++++++--- src/main/java/frc/robot/RobotContainer.java | 15 +- .../frc/robot/subsystems/turret/Turret.java | 91 ++++++-- .../frc/robot/subsystems/turret/TurretIO.java | 6 + .../turret/TurretIOSensorInputs.java | 91 +++++++- .../frc/robot/subsystems/vision/Vision.java | 8 +- .../vision/VisionIOPhotonCamera.java | 1 - .../subsystems/vision/photon/Camera.java | 5 +- 17 files changed, 526 insertions(+), 80 deletions(-) create mode 100644 src/main/java/frc/lib/state/StateMachine.java create mode 100644 src/main/java/frc/robot/BuildConstants.java create mode 100644 src/main/java/frc/robot/Constants.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b613e7f..d4dc9b2 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -5,8 +5,8 @@ - - + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 9eaa670..f5085a8 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + {}

It is advised to statically import this class (or one of its inner classes) wherever the + * constants are needed, to reduce verbosity. + */ +public final class Constants { + public static final Mode currentMode = Mode.REAL; + + public static enum Mode { + /** Running on a real robot. */ + REAL, + + /** Running a physics simulator. */ + SIM, + + /** Replaying from a log file. */ + REPLAY + } +} \ No newline at end of file diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index a079969..65ec142 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -1,75 +1,219 @@ -// 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. +// Copyright 2021-2024 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. package frc.robot; -import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.net.PortForwarder; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; -public class Robot extends TimedRobot { - private Command m_autonomousCommand; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import org.littletonrobotics.junction.LogFileUtil; +import org.littletonrobotics.junction.LoggedRobot; +import org.littletonrobotics.junction.Logger; +import org.littletonrobotics.junction.networktables.NT4Publisher; +import org.littletonrobotics.junction.wpilog.WPILOGReader; +import org.littletonrobotics.junction.wpilog.WPILOGWriter; + +/** + * The VM is configured to automatically run this class, and to call the functions 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 build.gradle file in the + * project. + */ +public class Robot extends LoggedRobot { + private Command autonomousCommand; + private RobotContainer robotContainer; + + private static final boolean IS_PRACTICE = true; + private static final String LOG_DIRECTORY = "/home/lvuser/logs"; + private static final long MIN_FREE_SPACE = + IS_PRACTICE + ? 100000000 + : // 100 MB + 1000000000; // 1 GB + + /** + * This function is run when the robot is first started up and should be used for any + * initialization code. + */ + @Override + public void robotInit() { + // Setup log directory and free up space if needed + SetupLog(); + //Pathfinding.setPathfinder(new LocalADStarAK()); + + // Record metadata + Logger.recordMetadata("ProjectName", BuildConstants.MAVEN_NAME); + Logger.recordMetadata("BuildDate", BuildConstants.BUILD_DATE); + Logger.recordMetadata("GitSHA", BuildConstants.GIT_SHA); + Logger.recordMetadata("GitDate", BuildConstants.GIT_DATE); + Logger.recordMetadata("GitBranch", BuildConstants.GIT_BRANCH); + switch (BuildConstants.DIRTY) { + case 0: + Logger.recordMetadata("GitDirty", "All changes committed"); + break; + case 1: + Logger.recordMetadata("GitDirty", "Uncomitted changes"); + break; + default: + Logger.recordMetadata("GitDirty", "Unknown"); + break; + } + + // Set up data receivers & replay source + switch (Constants.currentMode) { + case REAL: + // Running on a real robot, log to a USB stick ("/U/logs") + Logger.addDataReceiver(new WPILOGWriter(LOG_DIRECTORY)); + Logger.addDataReceiver(new NT4Publisher()); + break; + + case SIM: + // Running a physics simulator, log to NT + Logger.addDataReceiver(new NT4Publisher()); + break; + + case REPLAY: + // Replaying a log, set up replay source + setUseTiming(false); // Run as fast as possible + String logPath = LogFileUtil.findReplayLog(); + Logger.setReplaySource(new WPILOGReader(logPath)); + Logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(logPath, "_sim"))); + break; + } + + // See http://bit.ly/3YIzFZ6 for more information on timestamps in AdvantageKit. + // Logger.disableDeterministicTimestamps() - private final RobotContainer m_robotContainer; + // Start AdvantageKit logger + Logger.start(); + + // Instantiate our RobotContainer. This will perform all our button bindings, + // and put our autonomous chooser on the dashboard. + robotContainer = new RobotContainer(); + } + + void SetupLog() { + // Check if the log directory exists + var directory = new File(LOG_DIRECTORY); + if (!directory.exists()) { + directory.mkdir(); + } - public Robot() { - m_robotContainer = new RobotContainer(); + // ensure that there is enough space on the roboRIO to log data + if (directory.getFreeSpace() < MIN_FREE_SPACE) { + System.out.println("ERROR: out of space!"); + var files = directory.listFiles(); + if (files == null) { + System.out.println("ERROR: Cannot delete, Files are NULL!"); + } else { + // Sorting the files by name will ensure that the oldest files are deleted first + files = Arrays.stream(files).sorted().toArray(File[]::new); + + long bytesToDelete = MIN_FREE_SPACE - directory.getFreeSpace(); + + for (File file : files) { + if (file.getName().endsWith(".wpilog")) { + try { + bytesToDelete -= Files.size(file.toPath()); + } catch (IOException e) { + System.out.println("Failed to get size of file " + file.getName()); + continue; + } + if (file.delete()) { + System.out.println("Deleted " + file.getName() + " to free up space"); + } else { + System.out.println("Failed to delete " + file.getName()); + } + if (bytesToDelete <= 0) { + break; + } + } + } + } + } } + /** This function is called periodically during all modes. */ @Override public void robotPeriodic() { - CommandScheduler.getInstance().run(); + // Runs the Scheduler. This is responsible for polling buttons, adding + // newly-scheduled commands, running already-scheduled commands, removing + // finished or interrupted commands, and running subsystem periodic() methods. + // This must be called from the robot's periodic block in order for anything in + // the Command-based framework to work. + CommandScheduler.getInstance().run(); } + /** This function is called once when the robot is disabled. */ @Override public void disabledInit() {} + /** This function is called periodically when disabled. */ @Override public void disabledPeriodic() {} - @Override - public void disabledExit() {} - + /** This autonomous runs the autonomous command selected by your {@link RobotContainer} class. */ @Override public void autonomousInit() { - m_autonomousCommand = m_robotContainer.getAutonomousCommand(); - - if (m_autonomousCommand != null) { - CommandScheduler.getInstance().schedule(m_autonomousCommand); - } +// autonomousCommand = robotContainer.getAutonomousCommand(); +// +// // schedule the autonomous command (example) +// if (autonomousCommand != null) { +// autonomousCommand.schedule(); +// } } + /** This function is called periodically during autonomous. */ @Override public void autonomousPeriodic() {} - @Override - public void autonomousExit() {} - + /** This function is called once when teleop is enabled. */ @Override public void teleopInit() { - if (m_autonomousCommand != null) { - m_autonomousCommand.cancel(); + // This makes sure that the autonomous stops running when + // teleop starts running. If you want the autonomous to + // continue until interrupted by another command, remove + // this line or comment it out. + if (autonomousCommand != null) { + autonomousCommand.cancel(); } } + /** This function is called periodically during operator control. */ @Override public void teleopPeriodic() {} - @Override - public void teleopExit() {} - + /** This function is called once when test mode is enabled. */ @Override public void testInit() { + // Cancels all running commands at the start of test mode. CommandScheduler.getInstance().cancelAll(); } + /** This function is called periodically during test mode. */ @Override public void testPeriodic() {} + /** This function is called once when the robot is first started up. */ @Override - public void testExit() {} + public void simulationInit() {} + /** This function is called periodically whilst in simulation. */ @Override public void simulationPeriodic() {} -} +} \ No newline at end of file diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index a8addc2..06535d6 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -21,13 +21,14 @@ import frc.robot.generated.TunerConstants; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import frc.robot.subsystems.turret.Turret; import frc.robot.subsystems.vision.Vision; public class RobotContainer { private double MaxSpeed = TunerConstants.kSpeedAt12Volts.in(MetersPerSecond); // kSpeedAt12Volts desired top speed private double MaxAngularRate = RotationsPerSecond.of(0.75).in(RadiansPerSecond); // 3/4 of a rotation per second max angular velocity - private final SendableChooser autoChooser; +// private final SendableChooser autoChooser; /* Setting up bindings for necessary control of the swerve drive platform */ private final SwerveRequest.FieldCentric drive = new SwerveRequest.FieldCentric() @@ -42,11 +43,13 @@ public class RobotContainer { public final CommandSwerveDrivetrain drivetrain; private final Vision vision; + private final Turret turret; public RobotContainer() { drivetrain = TunerConstants.createDrivetrain(); - autoChooser = drivetrain.getAutoChooser(); - SmartDashboard.putData("Auto Path", autoChooser); + turret = new Turret(drivetrain); +// autoChooser = drivetrain.getAutoChooser(); +// SmartDashboard.putData("Auto Path", autoChooser); this.vision = new Vision(drivetrain); configureBindings(); @@ -90,7 +93,7 @@ private void configureBindings() { } - public Command getAutonomousCommand() { - return autoChooser.getSelected(); - } +// public Command getAutonomousCommand() { +// return autoChooser.getSelected(); +// } } diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 7ddf8ee..e23bb5d 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -1,43 +1,90 @@ package frc.robot.subsystems.turret; -import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.MathUtil; import frc.lib.FieldConstants; +import frc.lib.state.StateMachine; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; import org.littletonrobotics.junction.Logger; -public class Turret extends SpikeSystem { - private final TurretIO.TurretIOInputs inputs = new TurretIO.TurretIOInputs(); - private final TurretIOSensorInputs sensorData; +public class Turret extends SpikeSystem { + private static final double kTurretDeadbandDeg = 2.5; // degrees the turret must be within to be "on target" + + private TurretIOSensorInputs sensorData; private final CommandSwerveDrivetrain drive; + private enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } + + private final StateMachine tsm; + + private final TurretRequest turretRequest = new TurretRequest(); + + public static class TurretRequest { + public double targetAngleDegrees; + } + public Turret(CommandSwerveDrivetrain drive) { - super("Turret"); + super("Turret", new TurretIO.TurretIOInputs()); + Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; - this.sensorData = new TurretIOSensorInputs(null); // TODO: add camera + this.tsm = + StateMachine.forEnum(State.class) + .initial(State.IDLE) + .state(State.IDLE, cfg -> cfg + .onEnter(() -> { + // noop + }) + ) + + .state(State.TARGETING_HUB, cfg -> cfg + .onEnter(() -> { + double turretAngle = io.turretAngleDegrees; + double offset = sensorData.getAngleOffsetFromPose( + drive.getPose().getTranslation(), + FieldConstants.Hub.innerCenterPoint.toTranslation2d() + ); + turretRequest.targetAngleDegrees = turretAngle + offset; // == angleToHub + sensorData.setTurretAngle(turretRequest.targetAngleDegrees); + }) + .onTick(() -> { + double turretAbsDeg = io.turretAngleDegrees; + double poseOffsetDeg = sensorData.getAngleOffsetFromPose( + drive.getPose().getTranslation(), + FieldConstants.Hub.innerCenterPoint.toTranslation2d() + ); + + double targetDeg = getTargetDeg(turretAbsDeg, poseOffsetDeg); + turretRequest.targetAngleDegrees = targetDeg; + sensorData.setTurretAngle(targetDeg); + }) + ) + .build(); + } + + private double getTargetDeg(double turretAbsDeg, double poseOffsetDeg) { + double targetDeg = turretAbsDeg + poseOffsetDeg; // == angleToHub + + var yawOpt = sensorData.getErrorFromCamera(); + if (yawOpt.isPresent()) { + double yawErrDeg = yawOpt.get(); + + if (Math.abs(yawErrDeg) > kTurretDeadbandDeg) { + targetDeg = targetDeg - yawErrDeg; + } + } + + targetDeg = MathUtil.inputModulus(targetDeg, -180.0, 180.0); + return targetDeg; } @Override public void onPeriodic() { - Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); + tsm.tick(); } @Override protected Runnable setupDataRefresher() { - return useAsyncDataRefresher(inputs, sensorData); - } - - public double getAngleOffsetFromHub() { - Translation2d hubLocation = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - // get robot position - Translation2d robotPosition = drive.getPose().getTranslation(); - // calculate angle difference between turret and hub - double angleToHub = Math.toDegrees(Math.atan2(hubLocation.getY() - robotPosition.getY(), - hubLocation.getX() - robotPosition.getX())); - double turretAngle = inputs.turretAngleDegrees; - double angleOffset = angleToHub - turretAngle; - // normalize angle to [-180, 180] - angleOffset = ((angleOffset + 180) % 360) - 180; - return angleOffset; + sensorData = new TurretIOSensorInputs(null); + return useAsyncDataRefresher(sensorData); } } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIO.java b/src/main/java/frc/robot/subsystems/turret/TurretIO.java index 6a23472..3fbfbb0 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIO.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIO.java @@ -1,9 +1,12 @@ package frc.robot.subsystems.turret; +import edu.wpi.first.math.geometry.Translation2d; import frc.lib.subsystem.BaseIO; import frc.lib.subsystem.BaseInputClass; import org.littletonrobotics.junction.AutoLog; +import java.util.Optional; + public interface TurretIO extends BaseIO { @AutoLog @@ -11,4 +14,7 @@ public static class TurretIOInputs extends BaseInputClass { public double turretAngleDegrees = 0.0; public double offsetError = 0.0; // feed from camera, raw error value } + + abstract Optional getErrorFromCamera(); + abstract double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target); } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 2827884..a6362c4 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -1,12 +1,21 @@ package frc.robot.subsystems.turret; +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; import com.ctre.phoenix6.controls.MotionMagicVoltage; import com.ctre.phoenix6.hardware.CANcoder; import com.ctre.phoenix6.hardware.TalonFX; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.units.Units; +import edu.wpi.first.units.measure.Angle; import frc.lib.subsystem.IORefresher; import frc.robot.CanID; import frc.robot.subsystems.vision.photon.Camera; +import org.photonvision.targeting.PhotonPipelineResult; + +import java.util.List; +import java.util.Optional; public class TurretIOSensorInputs implements TurretIO, IORefresher { private final CANcoder turretEncoder; @@ -15,31 +24,99 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { private final MotionMagicVoltage mmRequest = new MotionMagicVoltage(0.0); - private static final double kTurretGearRatio = 100.0; // 100:1, this isn't the real value - + private static final double kTurretGearRatio = 100.0; // 100:1, replace with real value private static final double kTurretMinAngleDegrees = -180.0; private static final double kTurretMaxAngleDegrees = 180.0; + private static final int[] kValidTargetIDs = {9, 10, 23, 26}; + + private final StatusSignal turretAngleSignal; + + private Optional cachedYawErrorDeg = Optional.empty(); + private double cachedYawTimestampSec = Double.NEGATIVE_INFINITY; + public TurretIOSensorInputs(Camera camera) { this.turretEncoder = new CANcoder(CanID.TURRET_ENCODER.getID()); this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); this.turretCamera = camera; - } - public void setTurretAngle(double angle) { - double clamped = Math.max(kTurretMinAngleDegrees, Math.min(kTurretMaxAngleDegrees, angle)); - double motorRotations = (clamped / 360.0) * kTurretGearRatio; + this.turretAngleSignal = turretEncoder.getPosition(); + } + public void setTurretAngle(double angleDeg) { + double clampedDeg = Math.max(kTurretMinAngleDegrees, Math.min(kTurretMaxAngleDegrees, angleDeg)); + double motorRotations = (clampedDeg / 360.0) * kTurretGearRatio; turretMotor.setControl(mmRequest.withPosition(motorRotations)); } @Override public void updateInputs(TurretIOInputs inputs) { - inputs.turretAngleDegrees = turretEncoder.getPosition().getValue().in(Units.Degree); + inputs.turretAngleDegrees = turretAngleSignal.getValue().in(Units.Degree); } @Override public void refreshData() { + BaseStatusSignal.refreshAll(turretAngleSignal); + + cachedYawErrorDeg = Optional.empty(); + + if (turretCamera == null || turretCamera.getPhotonCamera() == null) return; + + List unread = turretCamera.getPhotonCamera().getAllUnreadResults(); + if (unread.isEmpty()) return; + + PhotonPipelineResult latest = null; + double latestTs = cachedYawTimestampSec; + + for (PhotonPipelineResult r : unread) { + double ts = r.getTimestampSeconds(); + if (ts > latestTs) { + latestTs = ts; + latest = r; + } + } + if (latest == null || !latest.hasTargets()) return; + + var validTargets = latest.getTargets().stream() + .filter(t -> { + int id = t.getFiducialId(); + for (int valid : kValidTargetIDs) { + if (id == valid) return true; + } + return false; + }) + .toList(); + if (validTargets.isEmpty()) return; + + double lowestAmbiguity = Double.MAX_VALUE; + double yawDeg = 0.0; + + for (var t : validTargets) { + double amb = t.getPoseAmbiguity(); + if (amb < lowestAmbiguity) { + lowestAmbiguity = amb; + yawDeg = t.getYaw(); + } + } + + cachedYawErrorDeg = Optional.of(yawDeg); + cachedYawTimestampSec = latestTs; + } + + @Override + public double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target) { + double angleToTargetDeg = Math.toDegrees( + Math.atan2(target.getY() - robotPose.getY(), target.getX() - robotPose.getX()) + ); + + double turretDeg = turretAngleSignal.getValue().in(Units.Degree); + + return MathUtil.inputModulus(angleToTargetDeg - turretDeg, -180.0, 180.0); + } + + @Override + public Optional getErrorFromCamera() { + return cachedYawErrorDeg; } } diff --git a/src/main/java/frc/robot/subsystems/vision/Vision.java b/src/main/java/frc/robot/subsystems/vision/Vision.java index 1dde862..86040fc 100644 --- a/src/main/java/frc/robot/subsystems/vision/Vision.java +++ b/src/main/java/frc/robot/subsystems/vision/Vision.java @@ -17,18 +17,13 @@ public Vision(CommandSwerveDrivetrain drive) { super("Vision", new VisionIO.VisionIOInputs()); this.drive = drive; - - System.out.println("IO!!!!!!!!!!!!!!!!!!!!: " + io.estimatedRobotPoses + "\n\n\n\n"); } @Override public void onPeriodic() { int index = 0; - System.out.println("Camera sizes " + CameraManager.getCameras().size()); - - System.out.println("Poses " + io.estimatedRobotPoses.size()); for (EstimatedRobotPose pose : io.estimatedRobotPoses) { - Logger.recordOutput("EstimatedPose/" + String.valueOf(index), pose.estimatedPose.toPose2d()); + Logger.recordOutput("EstimatedPose/" + index, pose.estimatedPose.toPose2d()); drive.addVisionMeasurement( pose.estimatedPose.toPose2d(), pose.timestampSeconds @@ -39,7 +34,6 @@ public void onPeriodic() { @Override protected Runnable setupDataRefresher() { - System.out.println("IO!!!!!!!!!!!!!!!!!!!!: " + io.estimatedRobotPoses + "\n\n\n\n"); photonCameras = new VisionIOPhotonCamera(); return useAsyncDataRefresher(photonCameras); } diff --git a/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonCamera.java b/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonCamera.java index a1fb552..e1b047e 100644 --- a/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonCamera.java +++ b/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonCamera.java @@ -19,7 +19,6 @@ public VisionIOPhotonCamera() { @Override public void refreshData() { - System.out.println("Refreshing data"); var newPoses = CameraManager.getCameras().stream() .map(Camera::getEstimatedRobotPose) .filter(Objects::nonNull) diff --git a/src/main/java/frc/robot/subsystems/vision/photon/Camera.java b/src/main/java/frc/robot/subsystems/vision/photon/Camera.java index 2a1ba86..58b817f 100644 --- a/src/main/java/frc/robot/subsystems/vision/photon/Camera.java +++ b/src/main/java/frc/robot/subsystems/vision/photon/Camera.java @@ -1,6 +1,7 @@ package frc.robot.subsystems.vision.photon; import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.wpilibj.Filesystem; import org.photonvision.EstimatedRobotPose; @@ -18,12 +19,12 @@ public class Camera { private transient final PhotonPoseEstimator poseEstimator; private final Transform3d cameraToRobot; - public Camera(String id, Transform3d cameraToRobot) throws IOException { + public Camera(String id, Transform3d cameraToRobot) { this.cameraToRobot = cameraToRobot; this.id = id; this.photonCamera = new PhotonCamera(id); this.poseEstimator = new PhotonPoseEstimator( - AprilTagFieldLayout.loadFromResource(Filesystem.getDeployDirectory().toPath().resolve("FRC2026_WELDED.fmap").toString()), + AprilTagFieldLayout.loadField(AprilTagFields.k2026RebuiltWelded), PhotonPoseEstimator.PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, cameraToRobot ); From 4d091493f3559327c3c22081ee396fafd3535d15 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 2 Feb 2026 12:37:21 -0500 Subject: [PATCH 03/19] Implement CRT and added controller capabilities for testing --- src/main/java/frc/lib/state/StateMachine.java | 2 +- src/main/java/frc/robot/RobotContainer.java | 30 +++++++++ .../frc/robot/subsystems/turret/Turret.java | 66 ++++--------------- .../frc/robot/subsystems/turret/TurretIO.java | 6 +- .../turret/TurretIOSensorInputs.java | 27 +++----- .../robot/subsystems/turret/TurretMath.java | 38 +++++++++++ 6 files changed, 95 insertions(+), 74 deletions(-) create mode 100644 src/main/java/frc/robot/subsystems/turret/TurretMath.java diff --git a/src/main/java/frc/lib/state/StateMachine.java b/src/main/java/frc/lib/state/StateMachine.java index c91d3b5..f799fb8 100644 --- a/src/main/java/frc/lib/state/StateMachine.java +++ b/src/main/java/frc/lib/state/StateMachine.java @@ -17,7 +17,7 @@ public StateMachine(E initialState, Map> stateNodes) { stateNodes.get(currentState).onEnter.run(); } - public static > Builder forEnum(Class enumClass) { + public static > Builder forEnum() { return new Builder<>(); } diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 06535d6..2bd141e 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -19,6 +19,7 @@ import edu.wpi.first.wpilibj2.command.button.RobotModeTriggers; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Direction; +import frc.lib.SpikeController; import frc.robot.generated.TunerConstants; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; import frc.robot.subsystems.turret.Turret; @@ -40,6 +41,7 @@ public class RobotContainer { private final Telemetry logger = new Telemetry(MaxSpeed); private final CommandXboxController joystick = new CommandXboxController(0); + private final SpikeController operator = new SpikeController(1, 0.05); public final CommandSwerveDrivetrain drivetrain; private final Vision vision; @@ -56,6 +58,34 @@ public RobotContainer() { } private void configureBindings() { + operator.y() + .onTrue(Commands.runOnce(() -> { + Turret.TurretRequest request = new Turret.TurretRequest(); + request.targetAngleDegrees = 0.0; + turret.runRequest(request); + })); + + operator.b() + .onTrue(Commands.runOnce(() -> { + Turret.TurretRequest request = new Turret.TurretRequest(); + request.targetAngleDegrees = 90.0; + turret.runRequest(request); + })); + + operator.a() + .onTrue(Commands.runOnce(() -> { + Turret.TurretRequest request = new Turret.TurretRequest(); + request.targetAngleDegrees = 180.0; + turret.runRequest(request); + })); + + operator.x() + .onTrue(Commands.runOnce(() -> { + Turret.TurretRequest request = new Turret.TurretRequest(); + request.targetAngleDegrees = -90.0; + turret.runRequest(request); + })); + // Note that X is defined as forward according to WPILib convention, // and Y is defined as to the left according to WPILib convention. drivetrain.setDefaultCommand( diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index e23bb5d..7e38027 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -2,22 +2,19 @@ import edu.wpi.first.math.MathUtil; import frc.lib.FieldConstants; -import frc.lib.state.StateMachine; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; import org.littletonrobotics.junction.Logger; public class Turret extends SpikeSystem { - private static final double kTurretDeadbandDeg = 2.5; // degrees the turret must be within to be "on target" + private static final double turretDeadbandDeg = 2.5; // degrees the turret must be within to be "on target" private TurretIOSensorInputs sensorData; private final CommandSwerveDrivetrain drive; private enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } - private final StateMachine tsm; - - private final TurretRequest turretRequest = new TurretRequest(); +// private final StateMachine tsm; public static class TurretRequest { public double targetAngleDegrees; @@ -27,59 +24,20 @@ public Turret(CommandSwerveDrivetrain drive) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; - this.tsm = - StateMachine.forEnum(State.class) - .initial(State.IDLE) - .state(State.IDLE, cfg -> cfg - .onEnter(() -> { - // noop - }) - ) - - .state(State.TARGETING_HUB, cfg -> cfg - .onEnter(() -> { - double turretAngle = io.turretAngleDegrees; - double offset = sensorData.getAngleOffsetFromPose( - drive.getPose().getTranslation(), - FieldConstants.Hub.innerCenterPoint.toTranslation2d() - ); - turretRequest.targetAngleDegrees = turretAngle + offset; // == angleToHub - sensorData.setTurretAngle(turretRequest.targetAngleDegrees); - }) - .onTick(() -> { - double turretAbsDeg = io.turretAngleDegrees; - double poseOffsetDeg = sensorData.getAngleOffsetFromPose( - drive.getPose().getTranslation(), - FieldConstants.Hub.innerCenterPoint.toTranslation2d() - ); - - double targetDeg = getTargetDeg(turretAbsDeg, poseOffsetDeg); - turretRequest.targetAngleDegrees = targetDeg; - sensorData.setTurretAngle(targetDeg); - }) - ) - .build(); - } - - private double getTargetDeg(double turretAbsDeg, double poseOffsetDeg) { - double targetDeg = turretAbsDeg + poseOffsetDeg; // == angleToHub - - var yawOpt = sensorData.getErrorFromCamera(); - if (yawOpt.isPresent()) { - double yawErrDeg = yawOpt.get(); - - if (Math.abs(yawErrDeg) > kTurretDeadbandDeg) { - targetDeg = targetDeg - yawErrDeg; - } - } - - targetDeg = MathUtil.inputModulus(targetDeg, -180.0, 180.0); - return targetDeg; } @Override public void onPeriodic() { - tsm.tick(); +// tsm.tick(); + } + + public void runRequest(TurretRequest request) { + double clampedAngle = MathUtil.clamp( + request.targetAngleDegrees, + -180.0, + 180.0 + ); + sensorData.setTurretAngle(clampedAngle); } @Override diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIO.java b/src/main/java/frc/robot/subsystems/turret/TurretIO.java index 3fbfbb0..afab967 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIO.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIO.java @@ -13,8 +13,10 @@ public interface TurretIO extends BaseIO { public static class TurretIOInputs extends BaseInputClass { public double turretAngleDegrees = 0.0; public double offsetError = 0.0; // feed from camera, raw error value + public double enc11 = 0.0; + public double enc13 = 0.0; } - abstract Optional getErrorFromCamera(); - abstract double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target); + Optional getErrorFromCamera(); + double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target); } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index a6362c4..efba01f 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -1,14 +1,10 @@ package frc.robot.subsystems.turret; -import com.ctre.phoenix6.BaseStatusSignal; -import com.ctre.phoenix6.StatusSignal; import com.ctre.phoenix6.controls.MotionMagicVoltage; -import com.ctre.phoenix6.hardware.CANcoder; import com.ctre.phoenix6.hardware.TalonFX; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.units.Units; -import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.wpilibj.DutyCycleEncoder; import frc.lib.subsystem.IORefresher; import frc.robot.CanID; import frc.robot.subsystems.vision.photon.Camera; @@ -18,9 +14,10 @@ import java.util.Optional; public class TurretIOSensorInputs implements TurretIO, IORefresher { - private final CANcoder turretEncoder; private final Camera turretCamera; private final TalonFX turretMotor; + private final DutyCycleEncoder enc11; + private final DutyCycleEncoder enc13; private final MotionMagicVoltage mmRequest = new MotionMagicVoltage(0.0); @@ -30,17 +27,14 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { private static final int[] kValidTargetIDs = {9, 10, 23, 26}; - private final StatusSignal turretAngleSignal; - private Optional cachedYawErrorDeg = Optional.empty(); private double cachedYawTimestampSec = Double.NEGATIVE_INFINITY; public TurretIOSensorInputs(Camera camera) { - this.turretEncoder = new CANcoder(CanID.TURRET_ENCODER.getID()); this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); this.turretCamera = camera; - - this.turretAngleSignal = turretEncoder.getPosition(); + this.enc11 = new DutyCycleEncoder(0); + this.enc13 = new DutyCycleEncoder(1); } public void setTurretAngle(double angleDeg) { @@ -51,13 +45,14 @@ public void setTurretAngle(double angleDeg) { @Override public void updateInputs(TurretIOInputs inputs) { - inputs.turretAngleDegrees = turretAngleSignal.getValue().in(Units.Degree); + inputs.enc11 = enc11.get(); + inputs.enc13 = enc13.get(); + double turretRotations = TurretMath.getTurretPosition(inputs.enc13, inputs.enc11); + inputs.turretAngleDegrees = TurretMath.positionToDegrees(turretRotations); } @Override public void refreshData() { - BaseStatusSignal.refreshAll(turretAngleSignal); - cachedYawErrorDeg = Optional.empty(); if (turretCamera == null || turretCamera.getPhotonCamera() == null) return; @@ -110,9 +105,7 @@ public double getAngleOffsetFromPose(Translation2d robotPose, Translation2d targ Math.atan2(target.getY() - robotPose.getY(), target.getX() - robotPose.getX()) ); - double turretDeg = turretAngleSignal.getValue().in(Units.Degree); - - return MathUtil.inputModulus(angleToTargetDeg - turretDeg, -180.0, 180.0); + return MathUtil.inputModulus(angleToTargetDeg, -180.0, 180.0); } @Override diff --git a/src/main/java/frc/robot/subsystems/turret/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/TurretMath.java new file mode 100644 index 0000000..4a36940 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/TurretMath.java @@ -0,0 +1,38 @@ +package frc.robot.subsystems.turret; + +public class TurretMath { + + private static double offsetDegrees = 0.0; + + private static double mod(double x, double m) { + return ((x % m) + m) % m; + } + + /** + * Computes absolute turret position using CRT. + * + * @param enc13 Encoder on 13:140 ratio, in [0,1) + * @param enc11 Encoder on 11:140 ratio, in [0,1) + * @return turret position in revolutions (mod 143) + */ + public static double getTurretPosition(double enc13, double enc11) { + double offsetRevs = offsetDegrees / 360.0; + + enc13 = mod(enc13 - offsetRevs, 1.0); + enc11 = mod(enc11 - offsetRevs, 1.0); + + double diff = enc11 - enc13; + + double k = mod(6.0 * diff, 11.0); + + return enc13 + 13.0 * k; + } + + public static double positionToDegrees(double positionRevs) { + return mod(positionRevs, 1.0) * 360.0; + } + + public static double positionToRadians(double positionRevs) { + return mod(positionRevs, 1.0) * 2.0 * Math.PI; + } +} From d3da2de798b661b23bf82c53e03c8ded1e46e3be Mon Sep 17 00:00:00 2001 From: NathanEdg Date: Sat, 7 Feb 2026 09:59:31 -0500 Subject: [PATCH 04/19] Turret rotation with motion magic profiler --- src/main/java/frc/robot/CanID.java | 3 +- src/main/java/frc/robot/RobotContainer.java | 13 +- .../frc/robot/subsystems/turret/Turret.java | 48 +++++- .../turret/TurretIOSensorInputs.java | 49 ++++-- .../robot/subsystems/turret/TurretMath.java | 149 ++++++++++++++++-- 5 files changed, 228 insertions(+), 34 deletions(-) diff --git a/src/main/java/frc/robot/CanID.java b/src/main/java/frc/robot/CanID.java index 2d4b3c6..276fad9 100644 --- a/src/main/java/frc/robot/CanID.java +++ b/src/main/java/frc/robot/CanID.java @@ -4,8 +4,7 @@ * Holder for all CAN device IDs besides drivetrain devices */ public enum CanID { - TURRET_ENCODER(11), - TURRET_MOTOR(12) + TURRET_MOTOR(13) ; private int deviceID; diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 2bd141e..ab89ad3 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -6,6 +6,8 @@ import static edu.wpi.first.units.Units.*; +import org.littletonrobotics.junction.Logger; + import com.ctre.phoenix6.swerve.SwerveModule.DriveRequestType; import com.pathplanner.lib.auto.AutoBuilder; import com.ctre.phoenix6.swerve.SwerveRequest; @@ -49,7 +51,7 @@ public class RobotContainer { public RobotContainer() { drivetrain = TunerConstants.createDrivetrain(); - turret = new Turret(drivetrain); + turret = new Turret(drivetrain, operator); // autoChooser = drivetrain.getAutoChooser(); // SmartDashboard.putData("Auto Path", autoChooser); @@ -62,6 +64,8 @@ private void configureBindings() { .onTrue(Commands.runOnce(() -> { Turret.TurretRequest request = new Turret.TurretRequest(); request.targetAngleDegrees = 0.0; + Logger.recordOutput("Turret/Request", request.targetAngleDegrees); + turret.runRequest(request); })); @@ -69,6 +73,7 @@ private void configureBindings() { .onTrue(Commands.runOnce(() -> { Turret.TurretRequest request = new Turret.TurretRequest(); request.targetAngleDegrees = 90.0; + Logger.recordOutput("Turret/Request", request.targetAngleDegrees); turret.runRequest(request); })); @@ -76,13 +81,17 @@ private void configureBindings() { .onTrue(Commands.runOnce(() -> { Turret.TurretRequest request = new Turret.TurretRequest(); request.targetAngleDegrees = 180.0; + Logger.recordOutput("Turret/Request", request.targetAngleDegrees); + turret.runRequest(request); })); operator.x() .onTrue(Commands.runOnce(() -> { Turret.TurretRequest request = new Turret.TurretRequest(); - request.targetAngleDegrees = -90.0; + request.targetAngleDegrees = 270; + Logger.recordOutput("Turret/Request", request.targetAngleDegrees); + turret.runRequest(request); })); diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 7e38027..15b835e 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -1,9 +1,15 @@ package frc.robot.subsystems.turret; import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.units.measure.Angle; import frc.lib.FieldConstants; +import frc.lib.SpikeController; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; + import org.littletonrobotics.junction.Logger; public class Turret extends SpikeSystem { @@ -11,6 +17,7 @@ public class Turret extends SpikeSystem { private TurretIOSensorInputs sensorData; private final CommandSwerveDrivetrain drive; + private final SpikeController controller; private enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } @@ -20,24 +27,51 @@ public static class TurretRequest { public double targetAngleDegrees; } - public Turret(CommandSwerveDrivetrain drive) { + public Turret(CommandSwerveDrivetrain drive, SpikeController controller) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; + this.controller = controller; } @Override public void onPeriodic() { // tsm.tick(); + Logger.recordOutput("Turret/enc11", io.enc11); + Logger.recordOutput("Turret/enc13", io.enc13); + Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); + // double angleDiff = sensorData.getAngleOffsetFromPose(drive.getPose().getTranslation(), FieldConstants.Hub.innerCenterPoint.toTranslation2d()); + // Logger.recordOutput("Turret/AngleDiff", angleDiff); + // double turretFieldAngleDeg = drive.getPose().getRotation().getDegrees() + io.turretAngleDegrees; + // Pose2d turretPose = drive.getPose().rotateBy(new Rotation2d(TurretMath.toRad(turretFieldAngleDeg))); + // Logger.recordOutput("Turret/TurretPose", turretPose); + // sensorData.setTurretAngle(angleDiff); + // double x = controller.getLeftX(); + // double y = -controller.getLeftY(); + + // double angleDeg = Math.toDegrees(Math.atan2(y, x)); + + // if (angleDeg < 0) { + // angleDeg += 360; + // } + + // Logger.recordOutput("Turret/RequestedAngle", angleDeg); + + // LocalTime now = LocalTime.now(); + // int seconds = now.getSecond(); + // Logger.recordOutput("Turret/TimeSec", seconds); + // double angle = seconds * 6; + + // Logger.recordOutput("Turret/RequestedAngle", angle); + + // TurretRequest req = new TurretRequest(); + // req.targetAngleDegrees = angleDeg; + // runRequest(req); + } public void runRequest(TurretRequest request) { - double clampedAngle = MathUtil.clamp( - request.targetAngleDegrees, - -180.0, - 180.0 - ); - sensorData.setTurretAngle(clampedAngle); + sensorData.setTurretAngle(request.targetAngleDegrees); } @Override diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index efba01f..44475a6 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -1,13 +1,20 @@ package frc.robot.subsystems.turret; +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.MotionMagicConfigs; +import com.ctre.phoenix6.configs.Slot0Configs; import com.ctre.phoenix6.controls.MotionMagicVoltage; import com.ctre.phoenix6.hardware.TalonFX; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.units.measure.Angle; import edu.wpi.first.wpilibj.DutyCycleEncoder; import frc.lib.subsystem.IORefresher; import frc.robot.CanID; import frc.robot.subsystems.vision.photon.Camera; + +import org.littletonrobotics.junction.Logger; import org.photonvision.targeting.PhotonPipelineResult; import java.util.List; @@ -19,11 +26,11 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { private final DutyCycleEncoder enc11; private final DutyCycleEncoder enc13; - private final MotionMagicVoltage mmRequest = new MotionMagicVoltage(0.0); + private final MotionMagicVoltage mmVoltage = new MotionMagicVoltage(0); + private final StatusSignal encoderSignal; - private static final double kTurretGearRatio = 100.0; // 100:1, replace with real value - private static final double kTurretMinAngleDegrees = -180.0; - private static final double kTurretMaxAngleDegrees = 180.0; + private static final double kTurretGearRatio = 140/10; + private static final double turretRotationOffset = -6.32; private static final int[] kValidTargetIDs = {9, 10, 23, 26}; @@ -32,27 +39,51 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { public TurretIOSensorInputs(Camera camera) { this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); + Slot0Configs config = new Slot0Configs(); + config.kP = 1; + config.kI = 0.01; + config.kD = 0.3; + config.kS = 0.194; + config.kV = 0.1167; + + MotionMagicConfigs mm = new MotionMagicConfigs(); + mm.MotionMagicAcceleration = 60; + mm.MotionMagicCruiseVelocity = 20; + mm.MotionMagicJerk = 0; + + this.turretMotor.getConfigurator().apply(mm); + this.turretMotor.getConfigurator().apply(config); this.turretCamera = camera; this.enc11 = new DutyCycleEncoder(0); this.enc13 = new DutyCycleEncoder(1); + + this.encoderSignal = turretMotor.getPosition(); } public void setTurretAngle(double angleDeg) { - double clampedDeg = Math.max(kTurretMinAngleDegrees, Math.min(kTurretMaxAngleDegrees, angleDeg)); - double motorRotations = (clampedDeg / 360.0) * kTurretGearRatio; - turretMotor.setControl(mmRequest.withPosition(motorRotations)); + double rotations = (angleDeg / 360) * kTurretGearRatio; + rotations = -rotations + turretRotationOffset; + + turretMotor.setControl(mmVoltage.withPosition(rotations)); + + Logger.recordOutput("Turret/TargetRotations", rotations); } @Override public void updateInputs(TurretIOInputs inputs) { inputs.enc11 = enc11.get(); inputs.enc13 = enc13.get(); - double turretRotations = TurretMath.getTurretPosition(inputs.enc13, inputs.enc11); - inputs.turretAngleDegrees = TurretMath.positionToDegrees(turretRotations); + double turretRotations = TurretMath.getTurretAngleRevs(inputs.enc13, inputs.enc11); + + inputs.turretAngleDegrees = TurretMath.toDegreesContinuous(turretRotations) - ((turretRotationOffset /14) * 360); + + Logger.recordOutput("Turret/CRTRevs", turretRotations); + Logger.recordOutput("Turret/EncoderRevs", -((encoderSignal.getValueAsDouble() - turretRotationOffset) / 14) * 360); } @Override public void refreshData() { + BaseStatusSignal.refreshAll(encoderSignal); cachedYawErrorDeg = Optional.empty(); if (turretCamera == null || turretCamera.getPhotonCamera() == null) return; diff --git a/src/main/java/frc/robot/subsystems/turret/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/TurretMath.java index 4a36940..4050677 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretMath.java @@ -1,38 +1,159 @@ package frc.robot.subsystems.turret; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.units.measure.Angle; + +/** + * Utility class for computing absolute turret position using two geared absolute encoders + * with the Chinese Remainder Theorem (CRT) approach. + * + * Hardware setup: + * - Turret gear: 140 teeth + * - Encoder 13: 13-tooth gear driving an absolute encoder (reads 0–1 rev) + * - Encoder 11: 11-tooth gear driving an absolute encoder (reads 0–1 rev) + * + * The combination gives unique positions over 11 × 13 = 143 teeth → 143/140 ≈ 1.0214 turret revolutions. + * Beyond that range, continuity tracking (unwrapping) is used. + */ public class TurretMath { - private static double offsetDegrees = 0.0; + private static final double GEAR_TEETH_TURRET = 140.0; + private static final double GEAR_TEETH_ENC13 = 13.0; + private static final double GEAR_TEETH_ENC11 = 11.0; + private static final double PERIOD_TEETH = GEAR_TEETH_ENC11 * GEAR_TEETH_ENC13; // 143 + private static final double PERIOD_REV = PERIOD_TEETH / GEAR_TEETH_TURRET; // ≈1.0214 + + private static double offsetDegrees = 0; // Calibration offset (degrees) + private static double lastPositionRevs = 0.0; // Unwrapped continuous position + private static boolean initialized = false; private static double mod(double x, double m) { return ((x % m) + m) % m; } /** - * Computes absolute turret position using CRT. + * Computes the raw absolute turret position in revolutions of the 13-tooth gear + * (i.e. position in "13-gear equivalent revolutions"). + * Range is approximately 0 to (143/13) ≈ 11.0 revolutions of the 13-gear. + * + * This uses a search-based CRT solution (most reliable for real hardware). * - * @param enc13 Encoder on 13:140 ratio, in [0,1) - * @param enc11 Encoder on 11:140 ratio, in [0,1) - * @return turret position in revolutions (mod 143) + * @param enc13 Absolute encoder on 13-tooth gear, normalized [0, 1) + * @param enc11 Absolute encoder on 11-tooth gear, normalized [0, 1) + * @return Raw turret position in 13-gear revolutions (multiply by 13/140 for turret revs) */ - public static double getTurretPosition(double enc13, double enc11) { + public static double getRawPosition13Revs(double enc13, double enc11) { double offsetRevs = offsetDegrees / 360.0; + // Apply offset and wrap to [0,1) enc13 = mod(enc13 - offsetRevs, 1.0); enc11 = mod(enc11 - offsetRevs, 1.0); - double diff = enc11 - enc13; + double bestError = Double.POSITIVE_INFINITY; + double bestPosition = 0.0; + + // Search over the 11 possible integer steps of the slower gear + for (int k = 0; k < (int) GEAR_TEETH_ENC11; k++) { + double assumed13Revs = enc13 + k; + // Predict what enc11 *should* read if this is the correct branch + double predicted11 = mod(assumed13Revs * (GEAR_TEETH_ENC13 / GEAR_TEETH_ENC11), 1.0); + + double error = Math.abs(predicted11 - enc11); + // Handle wrap-around distance + if (error > 0.5) { + error = 1.0 - error; + } + + if (error < bestError) { + bestError = error; + bestPosition = assumed13Revs; + } + } + + // You can add a sanity check here if desired + // if (bestError > 0.015) { /* signal invalid reading */ } + + return bestPosition; + } + + /** + * Returns the turret angle in revolutions, unwrapped for continuous multi-turn motion. + * Applies offset and continuity tracking. + * + * @param enc13 Absolute encoder on 13-tooth gear [0,1) + * @param enc11 Absolute encoder on 11-tooth gear [0,1) + * @return Continuous turret position in revolutions (can be >1 or <0) + */ + public static double getTurretAngleRevs(double enc13, double enc11) { + double raw13Revs = getRawPosition13Revs(enc13, enc11); + double rawTurretRevs = raw13Revs * (GEAR_TEETH_ENC13 / GEAR_TEETH_TURRET); + + // Apply offset again (in turret space) + rawTurretRevs -= offsetDegrees / 360.0; + + // Wrap raw reading into one period for comparison + double wrapped = mod(rawTurretRevs, PERIOD_REV); + + if (!initialized) { + lastPositionRevs = wrapped; + initialized = true; + return wrapped; + } + + // Compute shortest path delta (assuming small motion between calls) + double delta = wrapped - lastPositionRevs; + + // Unwrap using the known period + if (delta > PERIOD_REV / 2.0) { + delta -= PERIOD_REV; + } else if (delta < -PERIOD_REV / 2.0) { + delta += PERIOD_REV; + } - double k = mod(6.0 * diff, 11.0); + lastPositionRevs += delta; + return lastPositionRevs; + } + + /** + * Convert turret revolutions to degrees, wrapped to [0, 360). + * Use this when you only care about single-turn angle. + */ + public static double toDegreesWrapped(double turretRevs) { + return mod(turretRevs, 1.0) * 360.0; + } + + /** + * Convert turret revolutions to radians, wrapped to [0, 2π). + */ + public static double toRadiansWrapped(double turretRevs) { + return mod(turretRevs, 1.0) * 2.0 * Math.PI; + } + + /** + * Convert turret revolutions to total accumulated degrees (can be >360 or <0). + * Use this when you want continuous angle for PID or motion profiling. + */ + public static double toDegreesContinuous(double turretRevs) { + return turretRevs * 360.0; + } + + // Calibration / zeroing + public static void setOffsetDegrees(double degrees) { + offsetDegrees = degrees; + } - return enc13 + 13.0 * k; + public static double getOffsetDegrees() { + return offsetDegrees; } - public static double positionToDegrees(double positionRevs) { - return mod(positionRevs, 1.0) * 360.0; + // Reset continuity tracker (call on robot enable or after large jumps) + public static void resetContinuity() { + initialized = false; + lastPositionRevs = 0.0; } - public static double positionToRadians(double positionRevs) { - return mod(positionRevs, 1.0) * 2.0 * Math.PI; + public static double toRad(double angle) { + return angle * (Math.PI / 180); } -} +} \ No newline at end of file From b4013a4017d65d4445fce54b6cd177e3e64001f8 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 8 Feb 2026 17:58:27 -0500 Subject: [PATCH 05/19] Add angle filtering and shot compensation for turret subsystem --- src/main/java/frc/lib/AngleLowPassFilter.java | 71 +++++++++++++++++ .../frc/robot/subsystems/turret/Turret.java | 7 ++ .../turret/TurretIOSensorInputs.java | 24 +++++- .../turret/calc/ShotCompensation.java | 77 +++++++++++++++++++ .../subsystems/turret/calc/ShotData.java | 12 +++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/main/java/frc/lib/AngleLowPassFilter.java create mode 100644 src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java create mode 100644 src/main/java/frc/robot/subsystems/turret/calc/ShotData.java diff --git a/src/main/java/frc/lib/AngleLowPassFilter.java b/src/main/java/frc/lib/AngleLowPassFilter.java new file mode 100644 index 0000000..7949a76 --- /dev/null +++ b/src/main/java/frc/lib/AngleLowPassFilter.java @@ -0,0 +1,71 @@ +package frc.lib; + +public class AngleLowPassFilter { + + private double filteredX; + private double filteredY; + private boolean initialized; + + private double alpha; // smoothing factor (0..1) + + /** + * @param alpha smoothing factor. + * smaller = smoother but more lag. + * recommended starting range: 0.1 - 0.2 + */ + public AngleLowPassFilter(double alpha) { + setAlpha(alpha); + this.initialized = false; + } + + /** + * Update filter with new angle measurement. + * + * @param angleDeg angle in degrees + * @return filtered angle in degrees + */ + public double calculate(double angleDeg) { + double rad = Math.toRadians(angleDeg); + + double x = Math.cos(rad); + double y = Math.sin(rad); + + if (!initialized) { + filteredX = x; + filteredY = y; + initialized = true; + } + + filteredX += alpha * (x - filteredX); + filteredY += alpha * (y - filteredY); + + return Math.toDegrees(Math.atan2(filteredY, filteredX)); + } + + /** + * Reset filter state to a known angle. + */ + public void reset(double angleDeg) { + double rad = Math.toRadians(angleDeg); + filteredX = Math.cos(rad); + filteredY = Math.sin(rad); + initialized = true; + } + + /** + * Change smoothing strength dynamically. + */ + public void setAlpha(double alpha) { + if (alpha <= 0 || alpha > 1) { + throw new IllegalArgumentException("Alpha must be in (0, 1]"); + } + this.alpha = alpha; + } + + /** + * Whether filter has received first value. + */ + public boolean isInitialized() { + return initialized; + } +} diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 7e38027..99d191c 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -1,9 +1,13 @@ package frc.robot.subsystems.turret; import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; import frc.lib.FieldConstants; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import frc.robot.subsystems.turret.calc.ShotCompensation; import org.littletonrobotics.junction.Logger; public class Turret extends SpikeSystem { @@ -14,6 +18,8 @@ public class Turret extends SpikeSystem { private enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } + private Translation2d target = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + // private final StateMachine tsm; public static class TurretRequest { @@ -29,6 +35,7 @@ public Turret(CommandSwerveDrivetrain drive) { @Override public void onPeriodic() { // tsm.tick(); + ShotCompensation.AdjustedShot shot = ShotCompensation.compensateForMovement(drive.getPose(), drive.getState().Speeds, new Pose2d(target, new Rotation2d()), 0.6); } public void runRequest(TurretRequest request) { diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index efba01f..1d5f64e 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -1,10 +1,15 @@ package frc.robot.subsystems.turret; +import com.ctre.phoenix6.configs.MotionMagicConfigs; +import com.ctre.phoenix6.configs.MotorOutputConfigs; +import com.ctre.phoenix6.configs.Slot0Configs; import com.ctre.phoenix6.controls.MotionMagicVoltage; import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.NeutralModeValue; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.wpilibj.DutyCycleEncoder; +import frc.lib.AngleLowPassFilter; import frc.lib.subsystem.IORefresher; import frc.robot.CanID; import frc.robot.subsystems.vision.photon.Camera; @@ -19,6 +24,9 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { private final DutyCycleEncoder enc11; private final DutyCycleEncoder enc13; + private final AngleLowPassFilter turretAngleFilter = + new AngleLowPassFilter(0.15); + private final MotionMagicVoltage mmRequest = new MotionMagicVoltage(0.0); private static final double kTurretGearRatio = 100.0; // 100:1, replace with real value @@ -32,6 +40,18 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { public TurretIOSensorInputs(Camera camera) { this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); + MotionMagicConfigs mm = new MotionMagicConfigs(); + mm.MotionMagicCruiseVelocity = 20; + mm.MotionMagicAcceleration = 60; + mm.MotionMagicJerk = 0; + + this.turretMotor.getConfigurator().apply(mm); + + MotorOutputConfigs motorOutput = new MotorOutputConfigs(); + motorOutput.NeutralMode = NeutralModeValue.Brake; + + turretMotor.getConfigurator().apply(motorOutput); + this.turretCamera = camera; this.enc11 = new DutyCycleEncoder(0); this.enc13 = new DutyCycleEncoder(1); @@ -48,7 +68,9 @@ public void updateInputs(TurretIOInputs inputs) { inputs.enc11 = enc11.get(); inputs.enc13 = enc13.get(); double turretRotations = TurretMath.getTurretPosition(inputs.enc13, inputs.enc11); - inputs.turretAngleDegrees = TurretMath.positionToDegrees(turretRotations); + double rawAngle = TurretMath.positionToDegrees(turretRotations); + inputs.turretAngleDegrees = + turretAngleFilter.calculate(rawAngle); } @Override diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java new file mode 100644 index 0000000..bd61507 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java @@ -0,0 +1,77 @@ +package frc.robot.subsystems.turret.calc; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; + +public class ShotCompensation { + + public record AdjustedShot(double rpm, double hoodAngleDeg, double turretAngleDeg, + double turretFF_deg_s, double rangeFF_m_s) {} + + public static AdjustedShot compensateForMovement( + Pose2d robotPose, + ChassisSpeeds fieldRelVel, + Pose2d targetPose, + double nominalShotTimeS) { + + Translation2d toTarget = targetPose.getTranslation().minus(robotPose.getTranslation()); + double rangeM = toTarget.getNorm(); + + Rotation2d robotToGoalRot = toTarget.getAngle(); + + double uncompensatedTurretDeg = robotToGoalRot.getDegrees(); + + Translation2d velTrans = new Translation2d(fieldRelVel.vxMetersPerSecond, fieldRelVel.vyMetersPerSecond); + Translation2d velRelative = velTrans.rotateBy(robotToGoalRot.unaryMinus()); + + double radialM_s = velRelative.getX(); + double tangentialM_s = velRelative.getY(); + + double angularDeg_s = Math.toDegrees(fieldRelVel.omegaRadiansPerSecond); + + double effectiveRangeM; + double effectiveTurretDeg = uncompensatedTurretDeg; + + double shotHorizontalSpeed = rangeM / nominalShotTimeS - radialM_s; + if (shotHorizontalSpeed < 0) shotHorizontalSpeed = 0; + + double leadAdjustmentDeg = Math.toDegrees( + Math.atan2(-tangentialM_s, shotHorizontalSpeed) + ); + + effectiveTurretDeg += leadAdjustmentDeg; + + effectiveRangeM = nominalShotTimeS * + Math.hypot(tangentialM_s, shotHorizontalSpeed); + + double baseRPM = ShotData.distanceToRPM.get(effectiveRangeM); + double hoodDeg = ShotData.distanceToHoodAngle.get(effectiveRangeM); + + double turretFF_deg_s = -(angularDeg_s + Math.toDegrees(tangentialM_s / rangeM)); + double rangeFF_m_s = -radialM_s; + + // double adjustedRPM = baseRPM + rangeFF_m_s * someGain_RPM_per_m_s; // tune gain + + return new AdjustedShot(baseRPM, hoodDeg, effectiveTurretDeg, + turretFF_deg_s, rangeFF_m_s); + } + + private static double calculateVelocityCompensation( + ChassisSpeeds velocity, + Translation2d directionToTarget) { + + double robotSpeedTowardTarget = + velocity.vxMetersPerSecond * Math.cos(directionToTarget.getAngle().getRadians()) + + velocity.vyMetersPerSecond * Math.sin(directionToTarget.getAngle().getRadians()); + + // tune this constant empirically + return robotSpeedTowardTarget * 100.0; + } + + private static double rpmToVelocity(double rpm) { + // 6 inch wheel: 6 * 0.0254 = 0.1524 m diameter + return rpm * (Math.PI * 0.1524) / 60.0; + } +} \ No newline at end of file diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java new file mode 100644 index 0000000..0f6edcb --- /dev/null +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java @@ -0,0 +1,12 @@ +package frc.robot.subsystems.turret.calc; + +import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; + +public class ShotData { + public static final InterpolatingDoubleTreeMap distanceToRPM = new InterpolatingDoubleTreeMap(); + public static final InterpolatingDoubleTreeMap distanceToHoodAngle = new InterpolatingDoubleTreeMap(); + + static { + + } +} From 0029e57c5be519f7a0e1fb1282118c1253dac8fb Mon Sep 17 00:00:00 2001 From: NathanEdg Date: Mon, 9 Feb 2026 16:59:25 -0500 Subject: [PATCH 06/19] Aim to point --- src/main/java/frc/lib/Elastic.java | 390 ++++++++++++++++++ src/main/java/frc/robot/RobotContainer.java | 46 +-- .../frc/robot/subsystems/turret/Turret.java | 49 ++- .../turret/TurretIOSensorInputs.java | 94 ++++- .../robot/subsystems/turret/TurretMath.java | 14 + 5 files changed, 538 insertions(+), 55 deletions(-) create mode 100644 src/main/java/frc/lib/Elastic.java diff --git a/src/main/java/frc/lib/Elastic.java b/src/main/java/frc/lib/Elastic.java new file mode 100644 index 0000000..8d0cc95 --- /dev/null +++ b/src/main/java/frc/lib/Elastic.java @@ -0,0 +1,390 @@ +// Copyright (c) 2023-2026 Gold87 and other Elastic contributors +// This software can be modified and/or shared under the terms +// defined by the Elastic license: +// https://github.com/Gold872/elastic_dashboard/blob/main/LICENSE + +package frc.lib; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.PubSubOption; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.StringTopic; + +public final class Elastic { + private static final StringTopic notificationTopic = + NetworkTableInstance.getDefault().getStringTopic("/Elastic/RobotNotifications"); + private static final StringPublisher notificationPublisher = + notificationTopic.publish(PubSubOption.sendAll(true), PubSubOption.keepDuplicates(true)); + private static final StringTopic selectedTabTopic = + NetworkTableInstance.getDefault().getStringTopic("/Elastic/SelectedTab"); + private static final StringPublisher selectedTabPublisher = + selectedTabTopic.publish(PubSubOption.keepDuplicates(true)); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Represents the possible levels of notifications for the Elastic dashboard. These levels are + * used to indicate the severity or type of notification. + */ + public enum NotificationLevel { + /** Informational Message */ + INFO, + /** Warning message */ + WARNING, + /** Error message */ + ERROR + } + + /** + * Sends an notification to the Elastic dashboard. The notification is serialized as a JSON string + * before being published. + * + * @param notification the {@link Notification} object containing notification details + */ + public static void sendNotification(Notification notification) { + try { + notificationPublisher.set(objectMapper.writeValueAsString(notification)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + + /** + * Selects the tab of the dashboard with the given name. If no tab matches the name, this will + * have no effect on the widgets or tabs in view. + * + *

If the given name is a number, Elastic will select the tab whose index equals the number + * provided. + * + * @param tabName the name of the tab to select + */ + public static void selectTab(String tabName) { + selectedTabPublisher.set(tabName); + } + + /** + * Selects the tab of the dashboard at the given index. If this index is greater than or equal to + * the number of tabs, this will have no effect. + * + * @param tabIndex the index of the tab to select. + */ + public static void selectTab(int tabIndex) { + selectTab(Integer.toString(tabIndex)); + } + + /** + * Represents an notification object to be sent to the Elastic dashboard. This object holds + * properties such as level, title, description, display time, and dimensions to control how the + * notification is displayed on the dashboard. + */ + public static class Notification { + @JsonProperty("level") + private NotificationLevel level; + + @JsonProperty("title") + private String title; + + @JsonProperty("description") + private String description; + + @JsonProperty("displayTime") + private int displayTimeMillis; + + @JsonProperty("width") + private double width; + + @JsonProperty("height") + private double height; + + /** + * Creates a new Notification with all default parameters. This constructor is intended to be + * used with the chainable decorator methods + * + *

Title and description fields are empty. + */ + public Notification() { + this(NotificationLevel.INFO, "", ""); + } + + /** + * Creates a new Notification with all properties specified. + * + * @param level the level of the notification (e.g., INFO, WARNING, ERROR) + * @param title the title text of the notification + * @param description the descriptive text of the notification + * @param displayTimeMillis the time in milliseconds for which the notification is displayed + * @param width the width of the notification display area + * @param height the height of the notification display area, inferred if below zero + */ + public Notification( + NotificationLevel level, + String title, + String description, + int displayTimeMillis, + double width, + double height) { + this.level = level; + this.title = title; + this.displayTimeMillis = displayTimeMillis; + this.description = description; + this.height = height; + this.width = width; + } + + /** + * Creates a new Notification with default display time and dimensions. + * + * @param level the level of the notification + * @param title the title text of the notification + * @param description the descriptive text of the notification + */ + public Notification(NotificationLevel level, String title, String description) { + this(level, title, description, 3000, 350, -1); + } + + /** + * Creates a new Notification with a specified display time and default dimensions. + * + * @param level the level of the notification + * @param title the title text of the notification + * @param description the descriptive text of the notification + * @param displayTimeMillis the display time in milliseconds + */ + public Notification( + NotificationLevel level, String title, String description, int displayTimeMillis) { + this(level, title, description, displayTimeMillis, 350, -1); + } + + /** + * Creates a new Notification with specified dimensions and default display time. If the height + * is below zero, it is automatically inferred based on screen size. + * + * @param level the level of the notification + * @param title the title text of the notification + * @param description the descriptive text of the notification + * @param width the width of the notification display area + * @param height the height of the notification display area, inferred if below zero + */ + public Notification( + NotificationLevel level, String title, String description, double width, double height) { + this(level, title, description, 3000, width, height); + } + + /** + * Updates the level of this notification + * + * @param level the level to set the notification to + */ + public void setLevel(NotificationLevel level) { + this.level = level; + } + + /** + * @return the level of this notification + */ + public NotificationLevel getLevel() { + return level; + } + + /** + * Updates the title of this notification + * + * @param title the title to set the notification to + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Gets the title of this notification + * + * @return the title of this notification + */ + public String getTitle() { + return title; + } + + /** + * Updates the description of this notification + * + * @param description the description to set the notification to + */ + public void setDescription(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + /** + * Updates the display time of the notification + * + * @param seconds the number of seconds to display the notification for + */ + public void setDisplayTimeSeconds(double seconds) { + setDisplayTimeMillis((int) Math.round(seconds * 1000)); + } + + /** + * Updates the display time of the notification in milliseconds + * + * @param displayTimeMillis the number of milliseconds to display the notification for + */ + public void setDisplayTimeMillis(int displayTimeMillis) { + this.displayTimeMillis = displayTimeMillis; + } + + /** + * Gets the display time of the notification in milliseconds + * + * @return the number of milliseconds the notification is displayed for + */ + public int getDisplayTimeMillis() { + return displayTimeMillis; + } + + /** + * Updates the width of the notification + * + * @param width the width to set the notification to + */ + public void setWidth(double width) { + this.width = width; + } + + /** + * Gets the width of the notification + * + * @return the width of the notification + */ + public double getWidth() { + return width; + } + + /** + * Updates the height of the notification + * + *

If the height is set to -1, the height will be determined automatically by the dashboard + * + * @param height the height to set the notification to + */ + public void setHeight(double height) { + this.height = height; + } + + /** + * Gets the height of the notification + * + * @return the height of the notification + */ + public double getHeight() { + return height; + } + + /** + * Modifies the notification's level and returns itself to allow for method chaining + * + * @param level the level to set the notification to + * @return the current notification + */ + public Notification withLevel(NotificationLevel level) { + this.level = level; + return this; + } + + /** + * Modifies the notification's title and returns itself to allow for method chaining + * + * @param title the title to set the notification to + * @return the current notification + */ + public Notification withTitle(String title) { + setTitle(title); + return this; + } + + /** + * Modifies the notification's description and returns itself to allow for method chaining + * + * @param description the description to set the notification to + * @return the current notification + */ + public Notification withDescription(String description) { + setDescription(description); + return this; + } + + /** + * Modifies the notification's display time and returns itself to allow for method chaining + * + * @param seconds the number of seconds to display the notification for + * @return the current notification + */ + public Notification withDisplaySeconds(double seconds) { + return withDisplayMilliseconds((int) Math.round(seconds * 1000)); + } + + /** + * Modifies the notification's display time and returns itself to allow for method chaining + * + * @param displayTimeMillis the number of milliseconds to display the notification for + * @return the current notification + */ + public Notification withDisplayMilliseconds(int displayTimeMillis) { + setDisplayTimeMillis(displayTimeMillis); + return this; + } + + /** + * Modifies the notification's width and returns itself to allow for method chaining + * + * @param width the width to set the notification to + * @return the current notification + */ + public Notification withWidth(double width) { + setWidth(width); + return this; + } + + /** + * Modifies the notification's height and returns itself to allow for method chaining + * + * @param height the height to set the notification to + * @return the current notification + */ + public Notification withHeight(double height) { + setHeight(height); + return this; + } + + /** + * Modifies the notification's height and returns itself to allow for method chaining + * + *

This will set the height to -1 to have it automatically determined by the dashboard + * + * @return the current notification + */ + public Notification withAutomaticHeight() { + setHeight(-1); + return this; + } + + /** + * Modifies the notification to disable the auto dismiss behavior + * + *

This sets the display time to 0 milliseconds + * + *

The auto dismiss behavior can be re-enabled by setting the display time to a number + * greater than 0 + * + * @return the current notification + */ + public Notification withNoAutoDismiss() { + setDisplayTimeMillis(0); + return this; + } + } +} \ No newline at end of file diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index ab89ad3..33cfb9c 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -25,6 +25,7 @@ import frc.robot.generated.TunerConstants; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; import frc.robot.subsystems.turret.Turret; +import frc.robot.subsystems.turret.Turret.State; import frc.robot.subsystems.vision.Vision; public class RobotContainer { @@ -45,7 +46,7 @@ public class RobotContainer { private final CommandXboxController joystick = new CommandXboxController(0); private final SpikeController operator = new SpikeController(1, 0.05); - public final CommandSwerveDrivetrain drivetrain; + public static CommandSwerveDrivetrain drivetrain; private final Vision vision; private final Turret turret; @@ -59,41 +60,20 @@ public RobotContainer() { configureBindings(); } - private void configureBindings() { - operator.y() - .onTrue(Commands.runOnce(() -> { - Turret.TurretRequest request = new Turret.TurretRequest(); - request.targetAngleDegrees = 0.0; - Logger.recordOutput("Turret/Request", request.targetAngleDegrees); - - turret.runRequest(request); - })); - - operator.b() - .onTrue(Commands.runOnce(() -> { - Turret.TurretRequest request = new Turret.TurretRequest(); - request.targetAngleDegrees = 90.0; - Logger.recordOutput("Turret/Request", request.targetAngleDegrees); - turret.runRequest(request); - })); - - operator.a() - .onTrue(Commands.runOnce(() -> { - Turret.TurretRequest request = new Turret.TurretRequest(); - request.targetAngleDegrees = 180.0; - Logger.recordOutput("Turret/Request", request.targetAngleDegrees); - - turret.runRequest(request); - })); + public static CommandSwerveDrivetrain getDrive() { + return drivetrain; + } + private void configureBindings() { operator.x() - .onTrue(Commands.runOnce(() -> { - Turret.TurretRequest request = new Turret.TurretRequest(); - request.targetAngleDegrees = 270; - Logger.recordOutput("Turret/Request", request.targetAngleDegrees); + .onTrue(Commands.runOnce(() -> { + turret.switchState(State.TARGETING_HUB); + })); - turret.runRequest(request); - })); + operator.y() + .onTrue(Commands.runOnce(() -> { + turret.switchState(State.TARGETING_SHUTTLE); + })); // Note that X is defined as forward according to WPILib convention, // and Y is defined as to the left according to WPILib convention. diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 15b835e..ff704b3 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -3,10 +3,15 @@ import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.math.util.Units; import edu.wpi.first.units.measure.Angle; +import frc.lib.Elastic; import frc.lib.FieldConstants; import frc.lib.SpikeController; +import frc.lib.Elastic.Notification; +import frc.lib.Elastic.NotificationLevel; +import frc.lib.state.StateMachine; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; @@ -19,32 +24,59 @@ public class Turret extends SpikeSystem { private final CommandSwerveDrivetrain drive; private final SpikeController controller; - private enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } + public enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } -// private final StateMachine tsm; + private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + + private final StateMachine tsm; public static class TurretRequest { public double targetAngleDegrees; } + public void switchState(State state) { + tsm.transitionTo(state); + } + public Turret(CommandSwerveDrivetrain drive, SpikeController controller) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; this.controller = controller; + this.tsm = StateMachine.forEnum() + .initial(State.TARGETING_HUB) + .state(State.TARGETING_HUB, cfg -> { + cfg.onEnter(() -> { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") + ); + Elastic.selectTab("Scoring Mode"); + targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + }); + }) + .state(State.TARGETING_SHUTTLE, cfg -> { + cfg.onEnter(() -> { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") + ); + Elastic.selectTab("Shuttling Mode"); + targetPos = new Translation2d(); // 0,0 + }); + }) + .build(); } @Override public void onPeriodic() { -// tsm.tick(); + tsm.tick(); Logger.recordOutput("Turret/enc11", io.enc11); Logger.recordOutput("Turret/enc13", io.enc13); Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); // double angleDiff = sensorData.getAngleOffsetFromPose(drive.getPose().getTranslation(), FieldConstants.Hub.innerCenterPoint.toTranslation2d()); // Logger.recordOutput("Turret/AngleDiff", angleDiff); - // double turretFieldAngleDeg = drive.getPose().getRotation().getDegrees() + io.turretAngleDegrees; - // Pose2d turretPose = drive.getPose().rotateBy(new Rotation2d(TurretMath.toRad(turretFieldAngleDeg))); - // Logger.recordOutput("Turret/TurretPose", turretPose); + double turretFieldAngleDeg = io.turretAngleDegrees; + Pose2d turretPose = new Pose2d(drive.getPose().getTranslation(), new Rotation2d(TurretMath.toRad(turretFieldAngleDeg))); + Logger.recordOutput("Turret/TurretPose", turretPose); // sensorData.setTurretAngle(angleDiff); // double x = controller.getLeftX(); // double y = -controller.getLeftY(); @@ -68,6 +100,11 @@ public void onPeriodic() { // req.targetAngleDegrees = angleDeg; // runRequest(req); + double target = sensorData.getAngleOffsetFromPose(drive.getPose().getTranslation(), targetPos); + sensorData.setTurretAngle(target); + Logger.recordOutput("Turret/TargetOffset", target); + + // sensorData.setTurretAngle(0); } public void runRequest(TurretRequest request) { diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 44475a6..943b435 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -12,6 +12,7 @@ import edu.wpi.first.wpilibj.DutyCycleEncoder; import frc.lib.subsystem.IORefresher; import frc.robot.CanID; +import frc.robot.RobotContainer; import frc.robot.subsystems.vision.photon.Camera; import org.littletonrobotics.junction.Logger; @@ -30,7 +31,8 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { private final StatusSignal encoderSignal; private static final double kTurretGearRatio = 140/10; - private static final double turretRotationOffset = -6.32; + private final double turretRotationOffset; + private final double turretDegreesOffset = 246.7; private static final int[] kValidTargetIDs = {9, 10, 23, 26}; @@ -48,37 +50,92 @@ public TurretIOSensorInputs(Camera camera) { MotionMagicConfigs mm = new MotionMagicConfigs(); mm.MotionMagicAcceleration = 60; - mm.MotionMagicCruiseVelocity = 20; + mm.MotionMagicCruiseVelocity = 30; mm.MotionMagicJerk = 0; this.turretMotor.getConfigurator().apply(mm); - this.turretMotor.getConfigurator().apply(config); + // this.turretMotor.getConfigurator().apply(config); this.turretCamera = camera; this.enc11 = new DutyCycleEncoder(0); this.enc13 = new DutyCycleEncoder(1); this.encoderSignal = turretMotor.getPosition(); - } - public void setTurretAngle(double angleDeg) { - double rotations = (angleDeg / 360) * kTurretGearRatio; + double enc11InitialValue = enc11.get(); + double enc13InitialValue = enc13.get(); + + double turretRotations = TurretMath.getTurretAngleRevs(enc13InitialValue, enc11InitialValue); + + double turretDegrees = TurretMath.normalizeTurretHeading( + TurretMath.toDegreesWrapped(turretRotations), + turretDegreesOffset + ); + + Logger.recordOutput("Turret/InitialHeading", turretDegrees); + + double currentMotorRevs = turretMotor.getPosition().getValueAsDouble(); + double toZeroRevs = TurretMath.degreesToMotorPosition(turretDegrees); + + turretRotationOffset = currentMotorRevs + toZeroRevs; + + Logger.recordOutput("Turret/ToZeroRevs", toZeroRevs); + Logger.recordOutput("Turret/TurretRotationOffset", turretRotationOffset); + } + + public void setTurretAngle(double fieldAngleDeg) { + fieldAngleDeg = MathUtil.inputModulus(fieldAngleDeg, 0.0, 360.0); + + var drive = RobotContainer.getDrive(); + + double robotHeadingDeg = + MathUtil.inputModulus( + drive.getPose().getRotation().getDegrees(), + 0.0, 360.0 + ); + + double robotOmegaDegPerSec = + drive.getState().Speeds.omegaRadiansPerSecond + * 180.0 / Math.PI; + + double dt = 0.025; + double predictedHeadingDeg = + robotHeadingDeg + robotOmegaDegPerSec * dt; + + double turretAngleDeg = + MathUtil.inputModulus( + fieldAngleDeg + predictedHeadingDeg, + -180.0, 180.0 + ); + + double rotations = (turretAngleDeg / 360.0) * kTurretGearRatio; rotations = -rotations + turretRotationOffset; - turretMotor.setControl(mmVoltage.withPosition(rotations)); + double motorRps = + -(robotOmegaDegPerSec / 360.0) * kTurretGearRatio; - Logger.recordOutput("Turret/TargetRotations", rotations); - } + turretMotor.setControl( + mmVoltage + .withPosition(rotations) + .withFeedForward(motorRps * 0.1167) + ); + Logger.recordOutput("Turret/CommandedRotations", rotations); + + Logger.recordOutput("Turret/RobotOmegaDegPerSec", robotOmegaDegPerSec); + } + @Override public void updateInputs(TurretIOInputs inputs) { inputs.enc11 = enc11.get(); inputs.enc13 = enc13.get(); double turretRotations = TurretMath.getTurretAngleRevs(inputs.enc13, inputs.enc11); - inputs.turretAngleDegrees = TurretMath.toDegreesContinuous(turretRotations) - ((turretRotationOffset /14) * 360); + inputs.turretAngleDegrees = TurretMath.normalizeTurretHeading( + TurretMath.toDegreesWrapped(turretRotations), + turretDegreesOffset + ); Logger.recordOutput("Turret/CRTRevs", turretRotations); - Logger.recordOutput("Turret/EncoderRevs", -((encoderSignal.getValueAsDouble() - turretRotationOffset) / 14) * 360); } @Override @@ -131,12 +188,17 @@ public void refreshData() { } @Override - public double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target) { - double angleToTargetDeg = Math.toDegrees( - Math.atan2(target.getY() - robotPose.getY(), target.getX() - robotPose.getX()) + public double getAngleOffsetFromPose( + Translation2d robotPose, + Translation2d target + ) { + double fieldAngleDeg = Math.toDegrees( + Math.atan2( + target.getY() - robotPose.getY(), + target.getX() - robotPose.getX() + ) ); - - return MathUtil.inputModulus(angleToTargetDeg, -180.0, 180.0); + return MathUtil.inputModulus(-fieldAngleDeg, -180, 180); } @Override diff --git a/src/main/java/frc/robot/subsystems/turret/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/TurretMath.java index 4050677..f710a21 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretMath.java @@ -130,6 +130,20 @@ public static double toRadiansWrapped(double turretRevs) { return mod(turretRevs, 1.0) * 2.0 * Math.PI; } + public static double degreesToMotorPosition(double turretDegrees) { + return (turretDegrees * 14) / 360; + } + + public static double normalizeTurretHeading(double turretHeading, double zeroDegrees) { + double newHeading = turretHeading - zeroDegrees; + + if (newHeading < 0) { + newHeading = 360 - Math.abs(newHeading); + } + + return newHeading; + } + /** * Convert turret revolutions to total accumulated degrees (can be >360 or <0). * Use this when you want continuous angle for PID or motion profiling. From 1ec542141f4363b7729f669da8d024329b12fa70 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 11 Feb 2026 14:03:33 -0500 Subject: [PATCH 07/19] Refactor turret subsystem: remove controller dependency and improve shot compensation logic --- src/main/java/frc/robot/RobotContainer.java | 2 +- .../frc/robot/subsystems/turret/Turret.java | 80 +++++-------------- .../turret/TurretIOSensorInputs.java | 1 + .../turret/calc/ShotCompensation.java | 3 +- .../subsystems/turret/calc/ShotData.java | 2 +- .../turret/{ => calc}/TurretMath.java | 11 +-- 6 files changed, 29 insertions(+), 70 deletions(-) rename src/main/java/frc/robot/subsystems/turret/{ => calc}/TurretMath.java (95%) diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 33cfb9c..fed0127 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -52,7 +52,7 @@ public class RobotContainer { public RobotContainer() { drivetrain = TunerConstants.createDrivetrain(); - turret = new Turret(drivetrain, operator); + turret = new Turret(drivetrain); // autoChooser = drivetrain.getAutoChooser(); // SmartDashboard.putData("Auto Path", autoChooser); diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index ff704b3..d504faf 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -1,11 +1,8 @@ package frc.robot.subsystems.turret; -import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.units.measure.Angle; import frc.lib.Elastic; import frc.lib.FieldConstants; import frc.lib.SpikeController; @@ -15,54 +12,45 @@ import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import frc.robot.subsystems.turret.calc.ShotCompensation; +import frc.robot.subsystems.turret.calc.TurretMath; import org.littletonrobotics.junction.Logger; public class Turret extends SpikeSystem { - private static final double turretDeadbandDeg = 2.5; // degrees the turret must be within to be "on target" - private TurretIOSensorInputs sensorData; private final CommandSwerveDrivetrain drive; - private final SpikeController controller; - public enum State { IDLE, TARGETING_HUB, TARGETING_SHUTTLE, ZEROING, MANUAL_CONTROL } + public enum State { TARGETING_HUB, TARGETING_SHUTTLE, MANUAL_CONTROL } private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); private final StateMachine tsm; - public static class TurretRequest { - public double targetAngleDegrees; - } - public void switchState(State state) { tsm.transitionTo(state); } - public Turret(CommandSwerveDrivetrain drive, SpikeController controller) { + public Turret(CommandSwerveDrivetrain drive) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; - this.controller = controller; this.tsm = StateMachine.forEnum() .initial(State.TARGETING_HUB) - .state(State.TARGETING_HUB, cfg -> { - cfg.onEnter(() -> { + .state(State.TARGETING_HUB, cfg -> cfg + .onEnter(() -> { Elastic.sendNotification( new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") ); Elastic.selectTab("Scoring Mode"); targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - }); - }) - .state(State.TARGETING_SHUTTLE, cfg -> { - cfg.onEnter(() -> { - Elastic.sendNotification( - new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") - ); - Elastic.selectTab("Shuttling Mode"); - targetPos = new Translation2d(); // 0,0 - }); - }) + })) + .state(State.TARGETING_SHUTTLE, cfg -> cfg.onEnter(() -> { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") + ); + Elastic.selectTab("Shuttling Mode"); + targetPos = new Translation2d(); // 0,0 I think + })) .build(); } @@ -72,43 +60,19 @@ public void onPeriodic() { Logger.recordOutput("Turret/enc11", io.enc11); Logger.recordOutput("Turret/enc13", io.enc13); Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); - // double angleDiff = sensorData.getAngleOffsetFromPose(drive.getPose().getTranslation(), FieldConstants.Hub.innerCenterPoint.toTranslation2d()); - // Logger.recordOutput("Turret/AngleDiff", angleDiff); double turretFieldAngleDeg = io.turretAngleDegrees; Pose2d turretPose = new Pose2d(drive.getPose().getTranslation(), new Rotation2d(TurretMath.toRad(turretFieldAngleDeg))); Logger.recordOutput("Turret/TurretPose", turretPose); - // sensorData.setTurretAngle(angleDiff); - // double x = controller.getLeftX(); - // double y = -controller.getLeftY(); - - // double angleDeg = Math.toDegrees(Math.atan2(y, x)); - - // if (angleDeg < 0) { - // angleDeg += 360; - // } - // Logger.recordOutput("Turret/RequestedAngle", angleDeg); - - // LocalTime now = LocalTime.now(); - // int seconds = now.getSecond(); - // Logger.recordOutput("Turret/TimeSec", seconds); - // double angle = seconds * 6; - - // Logger.recordOutput("Turret/RequestedAngle", angle); - - // TurretRequest req = new TurretRequest(); - // req.targetAngleDegrees = angleDeg; - // runRequest(req); - - double target = sensorData.getAngleOffsetFromPose(drive.getPose().getTranslation(), targetPos); - sensorData.setTurretAngle(target); - Logger.recordOutput("Turret/TargetOffset", target); - - // sensorData.setTurretAngle(0); - } + ShotCompensation.AdjustedShot targetAngleCompensated = ShotCompensation.compensateForMovement( + drive.getPose(), + drive.getState().Speeds, + new Pose2d(targetPos, new Rotation2d()), + 0.3 // TODO: tune + ); - public void runRequest(TurretRequest request) { - sensorData.setTurretAngle(request.targetAngleDegrees); + sensorData.setTurretAngle(targetAngleCompensated.turretAngleDeg()); + Logger.recordOutput("Turret/TargetOffset", targetAngleCompensated.turretAngleDeg()); } @Override diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 943b435..bd7737a 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -13,6 +13,7 @@ import frc.lib.subsystem.IORefresher; import frc.robot.CanID; import frc.robot.RobotContainer; +import frc.robot.subsystems.turret.calc.TurretMath; import frc.robot.subsystems.vision.photon.Camera; import org.littletonrobotics.junction.Logger; diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java index bd61507..3506d97 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java @@ -14,7 +14,8 @@ public static AdjustedShot compensateForMovement( Pose2d robotPose, ChassisSpeeds fieldRelVel, Pose2d targetPose, - double nominalShotTimeS) { + double nominalShotTimeS + ) { Translation2d toTarget = targetPose.getTranslation().minus(robotPose.getTranslation()); double rangeM = toTarget.getNorm(); diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java index 0f6edcb..7b71344 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotData.java @@ -7,6 +7,6 @@ public class ShotData { public static final InterpolatingDoubleTreeMap distanceToHoodAngle = new InterpolatingDoubleTreeMap(); static { - + // TODO: get data and fill these in } } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java similarity index 95% rename from src/main/java/frc/robot/subsystems/turret/TurretMath.java rename to src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java index f710a21..ec74ad2 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java @@ -1,8 +1,4 @@ -package frc.robot.subsystems.turret; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.units.measure.Angle; +package frc.robot.subsystems.turret.calc; /** * Utility class for computing absolute turret position using two geared absolute encoders @@ -36,7 +32,7 @@ private static double mod(double x, double m) { * Computes the raw absolute turret position in revolutions of the 13-tooth gear * (i.e. position in "13-gear equivalent revolutions"). * Range is approximately 0 to (143/13) ≈ 11.0 revolutions of the 13-gear. - * + *

* This uses a search-based CRT solution (most reliable for real hardware). * * @param enc13 Absolute encoder on 13-tooth gear, normalized [0, 1) @@ -71,9 +67,6 @@ public static double getRawPosition13Revs(double enc13, double enc11) { } } - // You can add a sanity check here if desired - // if (bestError > 0.015) { /* signal invalid reading */ } - return bestPosition; } From a06609275fa54f8f341989bcd1fee92eceb6b2c4 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 20 Feb 2026 18:15:46 -0500 Subject: [PATCH 08/19] Refactor turret subsystem: clean up commented code, improve variable naming, and enhance logging for encoders --- src/main/deploy/vision/FRC2026_WELDED.fmap | 1 - src/main/java/frc/robot/Robot.java | 12 +- .../drive/CommandSwerveDrivetrain.java | 67 +++++---- .../frc/robot/subsystems/turret/Turret.java | 10 +- .../frc/robot/subsystems/turret/TurretIO.java | 8 +- .../turret/TurretIOSensorInputs.java | 88 ++--------- .../turret/calc/ShotCompensation.java | 35 ++++- .../subsystems/turret/calc/TurretMath.java | 137 ++++++++++-------- .../subsystems/vision/photon/Camera.java | 6 - 9 files changed, 165 insertions(+), 199 deletions(-) delete mode 100644 src/main/deploy/vision/FRC2026_WELDED.fmap diff --git a/src/main/deploy/vision/FRC2026_WELDED.fmap b/src/main/deploy/vision/FRC2026_WELDED.fmap deleted file mode 100644 index 6fa3319..0000000 --- a/src/main/deploy/vision/FRC2026_WELDED.fmap +++ /dev/null @@ -1 +0,0 @@ -{"fiducials":[{"family":"apriltag3_36h11_classic","id":1,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.6074798,1.2246467991473532e-16,-1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":2,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,3.644919399999999,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":3,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.0413645999999996,1.2246467991473532e-16,-1,0,0.35573759999999943,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":4,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.0413645999999996,1.2246467991473532e-16,-1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":5,"size":165.1,"transform":[-2.220446049250313e-16,1,0,3.644919399999999,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":6,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,3.6074798,1.2246467991473532e-16,-1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":7,"size":165.1,"transform":[1,0,0,3.6823844,0,1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":8,"size":165.1,"transform":[-2.220446049250313e-16,1,0,4.0005194,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":9,"size":165.1,"transform":[1,0,0,4.248677399999998,0,1,0,-0.35546240000000084,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":10,"size":165.1,"transform":[1,0,0,4.248677399999998,0,1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":11,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,4.0005194,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":12,"size":165.1,"transform":[1,0,0,3.6823844,0,1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":13,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.262817199999999,1.2246467991473532e-16,-1,0,3.368812599999999,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":14,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.262817199999999,1.2246467991473532e-16,-1,0,2.937012599999999,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":15,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.2624616,1.2246467991473532e-16,-1,0,0.2890625999999994,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":16,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,8.2624616,1.2246467991473532e-16,-1,0,-0.14273740000000057,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":17,"size":165.1,"transform":[1,0,0,-3.6074156000000004,0,1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":18,"size":165.1,"transform":[-2.220446049250313e-16,1,0,-3.6448806000000005,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":19,"size":165.1,"transform":[1,0,0,-3.041325800000001,0,1,0,-0.35546240000000084,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":20,"size":165.1,"transform":[1,0,0,-3.041325800000001,0,1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":21,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,-3.6448806000000005,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":22,"size":165.1,"transform":[1,0,0,-3.6074156000000004,0,1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":23,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-3.6823202000000004,1.2246467991473532e-16,-1,0,3.3902756,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":24,"size":165.1,"transform":[-2.220446049250313e-16,-1.0000000000000002,0,-4.0004806,1.0000000000000002,-2.220446049250313e-16,0,0.6035399999999989,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":25,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-4.2486386000000005,1.2246467991473532e-16,-1,0,0.35573759999999943,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":26,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-4.2486386000000005,1.2246467991473532e-16,-1,0,0.0001375999999995159,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":27,"size":165.1,"transform":[-2.220446049250313e-16,1,0,-4.0004806,-1,-2.220446049250313e-16,0,-0.6032648000000007,0,0,1,1.12395,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":28,"size":165.1,"transform":[-1,-1.2246467991473532e-16,0,-3.6823202000000004,1.2246467991473532e-16,-1,0,-3.3900004000000004,0,0,1,0.889,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":29,"size":165.1,"transform":[1,0,0,-8.262753,0,1,0,-3.3685374000000006,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":30,"size":165.1,"transform":[1,0,0,-8.262753,0,1,0,-2.9367374,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":31,"size":165.1,"transform":[1,0,0,-8.2624228,0,1,0,-0.2887874000000008,0,0,1,0.55245,0,0,0,1],"unique":true},{"family":"apriltag3_36h11_classic","id":32,"size":165.1,"transform":[1,0,0,-8.2624228,0,1,0,0.1430125999999996,0,0,1,0.55245,0,0,0,1],"unique":true}],"type":"frc","fieldlength":16.541,"fieldwidth":8.069,"pngBase64":"data:image/png;base64,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Uk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAMAUExURQAAAP///2VlZWZmZmpqamlpaWhoaGdnZwsGCNrV1y4aLp6KojoqPuro6wICtwICsQMD1gMDyQICpQICkQICbAQEvwkJ7QoK9gkJ4wcHrwQEUhQU7BYW9Q0NlxISxxERuBUV1xYWcCIikT4+/Tc33kVF/hcXPV9fiklJamdnk3FxoFxcfJKSvH9/pLS04AwMDRYWFzExMgMEKAIDEyIjMh0eKnJ0gl1falRVWzM1P1laXywwPigrNjk8R0FEThEVIRgbI09RV4SHjwQFB0dMUwkQGHuBiDU4O2ltcZaZm3R9ggUKDIGGiGJqbXJ2d1pfYCAnKAwrKyIxMTRJSS4/P0ZaWltwcFdoaFFgYGp7e2Rzc5uoqIqOjpKWlklKSpainyImIjpuMlKKRlZ2TgYKAgsLBGFhS1VVRWVlVltbT2xsX3R0a2BgWYeHf2ZmYI6OiIqKhW5uanp6dpKSjpaWkv78kE1LOfzqcxMRBoGAehkWBqSVTyklFTEuITYzJyIdC/HRXDMtGEI9KiknIOK9SbqdP6iOOpiCOHxpL414N7CMIJh5IYtvH7GMKaCAJbqULaiHKWtWGn1kH2FOGEE0EMOcMXRdHVZFFpd6J4RrI7COL45zJks9FJ6BK8ukONWuP2NTJlNGIT41GkY8H1pZVi4kDDcsDzs5NBwZE0dANQcEAiUbFSYfGzsyLTMiIb4CArYCArECAqUCApQCAtMEBHcDAzoCAvcJCVEEBOYMDM0NDbwMDB8CAqwMDJ4MDOwUFPwWFhICAq8cHP0qKsIiIuYqKv01Na0lJYseHlgVFSoNDUAYGHhTU4dfX2dJSZFoaFlAQJxycriQkKWBgXdhYd62toBqamVVVVFGRoB6em9qapyWlmpmZnp2doqGhoaCgpaSkrOxscHBwaWlpZubm5aWlpKSko6OjoqKioaGhoKCgn5+fnp6enZ2dnJycm5ubmJiYl5eXlpaWlZWVlJSUk5OTkNDQzs7OzY2NisrKyYmJiIiIhwcHBISEgICAv////17gRMAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAAsTAAALEwEAmpwYAAC6uUlEQVR4nNR9d4AU9dn/55mZnS2z5doeTSkxhpJiN2JMFBEEC5jYYo0NBDVG42Hhou8vMXdGPTQaFQU0JjFFkxiBKIhiefNG7OVNXpAYBRQO7vZuy+1tnfL8/piZLXe7e7tHkTwHd7tTvt/vzDzzPJ/v074kCWAGQtgNCpp/QgWfc9+rOLvouGD+zNv7X0h/4dDbqXgvCVd86+aUS7/rf1aAjYG9MADCQOLBvVLFvYMOLXEM55sxFE1AUqzQUK673DFtuX2Lrb/taLX2LB54cGEr7dYpXOIQAqi9lb9/1ggy5HvvF5nBQMJj77+r8zq165FfCea3pLU9ASWhAEjAA6Dl7oGD7wCAGwZd000AgJ8N2l5MXHivK99q4onnUioQYgANQzRbmcJAYRvm12G2Gc6d94Pkn7MjT1puWF/ZbJgQvPkhDWLn9XcADDQUMSATJGgF1z2YOwEAkma1SQAgFu7Kn0wAaZCgARDBFe5nYpejITGiYMPglwwg8/xB72muVWIQwDSoHy68Fvv9sA9qs9kX7a3mmPmas6gOmUeXisxA2+J8e8b3b4jW/fznVP51NJky/w2AwsRUfFgLgA4M3m7ybgnmzdFNt99akoVvvv3WOwHjSxIMDGx0t6kB4eEzuHWm9VQCA3cTsGA1AEAyNxR3RRIAKc+RVHBkEUkANJC5r+gG5G8HE1g0n53ElfgxBMHsIxREKBgKhoLFu80NA9ixkA/z/aHocdicyNZPfhvbPNXWmuOLxfkTvfm2C/gRDKgCQFzAkcQFY0goRRepgImR9GAA53UAYFp098DtaLH2l2XJO7MjSm2+1xpLr1DifaydGhpQLBB3T+CaRLcBiJXYce3pAAANpeSfKfns7WX50SKpwj4AzNZRlbQxAAYSDABBIIggggP2h4KhkMmJheKR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"} \ No newline at end of file diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 65ec142..530ef86 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -170,12 +170,12 @@ public void disabledPeriodic() {} /** This autonomous runs the autonomous command selected by your {@link RobotContainer} class. */ @Override public void autonomousInit() { -// autonomousCommand = robotContainer.getAutonomousCommand(); -// -// // schedule the autonomous command (example) -// if (autonomousCommand != null) { -// autonomousCommand.schedule(); -// } + autonomousCommand = robotContainer.getAutonomousCommand(); + + // schedule the autonomous command (example) + if (autonomousCommand != null) { + CommandScheduler.getInstance().schedule(autonomousCommand); + } } /** This function is called periodically during autonomous. */ diff --git a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java index adc3275..30a2fbf 100644 --- a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java +++ b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java @@ -303,40 +303,39 @@ public void addVisionMeasurement( } private void configureAutoBuilder() { -// try { -// RobotConfig config = RobotConfig.fromGUISettings(); -// AutoBuilder.configure( -// () -> getState().Pose, -// this::resetPose, -// () -> getState().Speeds, -// (speeds, feedForwards) -> setControl( -// m_pathApplyRobotSpeeds.withSpeeds(speeds) -// .withWheelForceFeedforwardsX(feedForwards.robotRelativeForcesXNewtons()) -// .withWheelForceFeedforwardsY(feedForwards.robotRelativeForcesYNewtons()) -// ), -// new PPHolonomicDriveController( -// new PIDConstants(5.0, 0, 0, 0), -// new PIDConstants(5.0, 0, 0, 0) -// ), -// config, -// () -> false, -// this -// ); -// Pathfinding.setPathfinder(new LocalADStarAK()); -// -// PathPlannerLogging.setLogActivePathCallback( -// (activePath) -> { -// Logger.recordOutput( -// "Odometry/Trajectory", activePath.toArray(new Pose2d[0])); -// }); -// PathPlannerLogging.setLogTargetPoseCallback( -// (targetPose) -> { -// Logger.recordOutput("Odometry/TrajectorySetpoint", targetPose); -// }); -// } catch (Exception e) { -// DriverStation.reportError("Failed to load PathPlanner config and configure AutoBuilder", e.getStackTrace()); -// } - + try { + RobotConfig config = RobotConfig.fromGUISettings(); + AutoBuilder.configure( + () -> getState().Pose, + this::resetPose, + () -> getState().Speeds, + (speeds, feedForwards) -> setControl( + m_pathApplyRobotSpeeds.withSpeeds(speeds) + .withWheelForceFeedforwardsX(feedForwards.robotRelativeForcesXNewtons()) + .withWheelForceFeedforwardsY(feedForwards.robotRelativeForcesYNewtons()) + ), + new PPHolonomicDriveController( + new PIDConstants(5.0, 0, 0, 0), + new PIDConstants(5.0, 0, 0, 0) + ), + config, + () -> false, + this + ); + Pathfinding.setPathfinder(new LocalADStarAK()); + + PathPlannerLogging.setLogActivePathCallback( + (activePath) -> { + Logger.recordOutput( + "Odometry/Trajectory", activePath.toArray(new Pose2d[0])); + }); + PathPlannerLogging.setLogTargetPoseCallback( + (targetPose) -> { + Logger.recordOutput("Odometry/TrajectorySetpoint", targetPose); + }); + } catch (Exception e) { + DriverStation.reportError("Failed to load PathPlanner config and configure AutoBuilder", e.getStackTrace()); + } } public Pose2d getPose() { diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index d504faf..52fcf47 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -20,7 +20,7 @@ public class Turret extends SpikeSystem { private TurretIOSensorInputs sensorData; private final CommandSwerveDrivetrain drive; - public enum State { TARGETING_HUB, TARGETING_SHUTTLE, MANUAL_CONTROL } + public enum State { TARGETING_HUB, TARGETING_SHUTTLE } private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); @@ -49,7 +49,7 @@ public Turret(CommandSwerveDrivetrain drive) { new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") ); Elastic.selectTab("Shuttling Mode"); - targetPos = new Translation2d(); // 0,0 I think + targetPos = new Translation2d(); })) .build(); } @@ -57,8 +57,8 @@ public Turret(CommandSwerveDrivetrain drive) { @Override public void onPeriodic() { tsm.tick(); - Logger.recordOutput("Turret/enc11", io.enc11); - Logger.recordOutput("Turret/enc13", io.enc13); + Logger.recordOutput("Turret/PinionEncoder", io.pinionEncoder); + Logger.recordOutput("Turret/FollowerEncoder", io.followerEncoder); Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); double turretFieldAngleDeg = io.turretAngleDegrees; Pose2d turretPose = new Pose2d(drive.getPose().getTranslation(), new Rotation2d(TurretMath.toRad(turretFieldAngleDeg))); @@ -77,7 +77,7 @@ public void onPeriodic() { @Override protected Runnable setupDataRefresher() { - sensorData = new TurretIOSensorInputs(null); + sensorData = new TurretIOSensorInputs(); return useAsyncDataRefresher(sensorData); } } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIO.java b/src/main/java/frc/robot/subsystems/turret/TurretIO.java index afab967..8d7234e 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIO.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIO.java @@ -5,18 +5,14 @@ import frc.lib.subsystem.BaseInputClass; import org.littletonrobotics.junction.AutoLog; -import java.util.Optional; - public interface TurretIO extends BaseIO { @AutoLog public static class TurretIOInputs extends BaseInputClass { public double turretAngleDegrees = 0.0; - public double offsetError = 0.0; // feed from camera, raw error value - public double enc11 = 0.0; - public double enc13 = 0.0; + public double pinionEncoder = 0.0; + public double followerEncoder = 0.0; } - Optional getErrorFromCamera(); double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target); } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index bd7737a..34e3dba 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -23,10 +23,9 @@ import java.util.Optional; public class TurretIOSensorInputs implements TurretIO, IORefresher { - private final Camera turretCamera; private final TalonFX turretMotor; - private final DutyCycleEncoder enc11; - private final DutyCycleEncoder enc13; + private final DutyCycleEncoder pinionEncoder; + private final DutyCycleEncoder followerEncoder; private final MotionMagicVoltage mmVoltage = new MotionMagicVoltage(0); private final StatusSignal encoderSignal; @@ -35,37 +34,25 @@ public class TurretIOSensorInputs implements TurretIO, IORefresher { private final double turretRotationOffset; private final double turretDegreesOffset = 246.7; - private static final int[] kValidTargetIDs = {9, 10, 23, 26}; - - private Optional cachedYawErrorDeg = Optional.empty(); - private double cachedYawTimestampSec = Double.NEGATIVE_INFINITY; - - public TurretIOSensorInputs(Camera camera) { + public TurretIOSensorInputs() { this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); - Slot0Configs config = new Slot0Configs(); - config.kP = 1; - config.kI = 0.01; - config.kD = 0.3; - config.kS = 0.194; - config.kV = 0.1167; - + MotionMagicConfigs mm = new MotionMagicConfigs(); mm.MotionMagicAcceleration = 60; mm.MotionMagicCruiseVelocity = 30; mm.MotionMagicJerk = 0; this.turretMotor.getConfigurator().apply(mm); - // this.turretMotor.getConfigurator().apply(config); - this.turretCamera = camera; - this.enc11 = new DutyCycleEncoder(0); - this.enc13 = new DutyCycleEncoder(1); + this.pinionEncoder = new DutyCycleEncoder(0); + this.followerEncoder = new DutyCycleEncoder(1); this.encoderSignal = turretMotor.getPosition(); - double enc11InitialValue = enc11.get(); - double enc13InitialValue = enc13.get(); + double pinionEncoderValue = pinionEncoder.get(); + double followerEncoderValue = followerEncoder.get(); - double turretRotations = TurretMath.getTurretAngleRevs(enc13InitialValue, enc11InitialValue); + // set offset of the turret on startup + double turretRotations = TurretMath.getTurretAngleRevs(pinionEncoderValue, followerEncoderValue); double turretDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), @@ -127,9 +114,9 @@ public void setTurretAngle(double fieldAngleDeg) { @Override public void updateInputs(TurretIOInputs inputs) { - inputs.enc11 = enc11.get(); - inputs.enc13 = enc13.get(); - double turretRotations = TurretMath.getTurretAngleRevs(inputs.enc13, inputs.enc11); + inputs.pinionEncoder = this.pinionEncoder.get(); + inputs.followerEncoder = this.followerEncoder.get(); + double turretRotations = TurretMath.getTurretAngleRevs(inputs.pinionEncoder, inputs.followerEncoder); inputs.turretAngleDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), @@ -142,50 +129,6 @@ public void updateInputs(TurretIOInputs inputs) { @Override public void refreshData() { BaseStatusSignal.refreshAll(encoderSignal); - cachedYawErrorDeg = Optional.empty(); - - if (turretCamera == null || turretCamera.getPhotonCamera() == null) return; - - List unread = turretCamera.getPhotonCamera().getAllUnreadResults(); - if (unread.isEmpty()) return; - - PhotonPipelineResult latest = null; - double latestTs = cachedYawTimestampSec; - - for (PhotonPipelineResult r : unread) { - double ts = r.getTimestampSeconds(); - if (ts > latestTs) { - latestTs = ts; - latest = r; - } - } - - if (latest == null || !latest.hasTargets()) return; - - var validTargets = latest.getTargets().stream() - .filter(t -> { - int id = t.getFiducialId(); - for (int valid : kValidTargetIDs) { - if (id == valid) return true; - } - return false; - }) - .toList(); - if (validTargets.isEmpty()) return; - - double lowestAmbiguity = Double.MAX_VALUE; - double yawDeg = 0.0; - - for (var t : validTargets) { - double amb = t.getPoseAmbiguity(); - if (amb < lowestAmbiguity) { - lowestAmbiguity = amb; - yawDeg = t.getYaw(); - } - } - - cachedYawErrorDeg = Optional.of(yawDeg); - cachedYawTimestampSec = latestTs; } @Override @@ -201,9 +144,4 @@ public double getAngleOffsetFromPose( ); return MathUtil.inputModulus(-fieldAngleDeg, -180, 180); } - - @Override - public Optional getErrorFromCamera() { - return cachedYawErrorDeg; - } } diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java index 3506d97..1479001 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java @@ -17,44 +17,65 @@ public static AdjustedShot compensateForMovement( double nominalShotTimeS ) { + // vector from robot to target in field coords Translation2d toTarget = targetPose.getTranslation().minus(robotPose.getTranslation()); + // straight line distance to target (meters) double rangeM = toTarget.getNorm(); + // angle from robot to goal, in field/world space Rotation2d robotToGoalRot = toTarget.getAngle(); + // raw turret angle pointing directly at the goal (no movement compensation) double uncompensatedTurretDeg = robotToGoalRot.getDegrees(); + // convert chassis velocities into a 2d translation for easier math Translation2d velTrans = new Translation2d(fieldRelVel.vxMetersPerSecond, fieldRelVel.vyMetersPerSecond); + // rotate the velocity vector into the frame that points toward the goal + // this makes the x component be the radial (toward/away) speed and y be tangential (sideways) Translation2d velRelative = velTrans.rotateBy(robotToGoalRot.unaryMinus()); + // radial: how fast robot is moving toward/away from the target (m/s) double radialM_s = velRelative.getX(); + // tangential: sideways speed relative to the target (m/s) double tangentialM_s = velRelative.getY(); + // convert chassis angular velocity to degrees per second so it's easier to reason about double angularDeg_s = Math.toDegrees(fieldRelVel.omegaRadiansPerSecond); double effectiveRangeM; + // start with the angle you'd aim if the robot were stationary double effectiveTurretDeg = uncompensatedTurretDeg; + // estimate horizontal shot speed needed to reach target in nominalShotTimeS, then remove radial robot motion double shotHorizontalSpeed = rangeM / nominalShotTimeS - radialM_s; - if (shotHorizontalSpeed < 0) shotHorizontalSpeed = 0; + // if robot is moving faster than the shot would need, clamp so we don't divide by negative speeds + if (shotHorizontalSpeed < 0) { + shotHorizontalSpeed = 0; + } + // lead angle: how far to lead the shot based on sideways motion vs horizontal shot speed + // atan2(-tangential, shotSpeed) gives the angular correction to point into the moving target double leadAdjustmentDeg = Math.toDegrees( Math.atan2(-tangentialM_s, shotHorizontalSpeed) ); + // apply the lead so turret points where the target will be effectiveTurretDeg += leadAdjustmentDeg; + // recompute the effective distance the ball will travel taking the actual shot speed and tangential motion into account effectiveRangeM = nominalShotTimeS * Math.hypot(tangentialM_s, shotHorizontalSpeed); + // lookup base rpm and hood angle from empirical shot table for the effective range double baseRPM = ShotData.distanceToRPM.get(effectiveRangeM); double hoodDeg = ShotData.distanceToHoodAngle.get(effectiveRangeM); + // feedforward for turret angular motion: combine robot yaw and the apparent angular rate due to tangential motion + // note: tangential/range gives radians/sec approx, convert to deg/s double turretFF_deg_s = -(angularDeg_s + Math.toDegrees(tangentialM_s / rangeM)); + // feedforward for range: negative radial means robot moving toward target, so subtract double rangeFF_m_s = -radialM_s; - // double adjustedRPM = baseRPM + rangeFF_m_s * someGain_RPM_per_m_s; // tune gain - return new AdjustedShot(baseRPM, hoodDeg, effectiveTurretDeg, turretFF_deg_s, rangeFF_m_s); } @@ -63,16 +84,18 @@ private static double calculateVelocityCompensation( ChassisSpeeds velocity, Translation2d directionToTarget) { + // project robot velocity onto the direction to target to get speed toward target double robotSpeedTowardTarget = velocity.vxMetersPerSecond * Math.cos(directionToTarget.getAngle().getRadians()) + velocity.vyMetersPerSecond * Math.sin(directionToTarget.getAngle().getRadians()); - // tune this constant empirically + // multiply by 100 to convert to whatever unit the caller expects (legacy scaling) return robotSpeedTowardTarget * 100.0; } private static double rpmToVelocity(double rpm) { - // 6 inch wheel: 6 * 0.0254 = 0.1524 m diameter - return rpm * (Math.PI * 0.1524) / 60.0; + // convert wheel rpm to linear m/s + // wheel diameter 4 inches => 0.1016 m + return rpm * (Math.PI * 0.1016) / 60.0; } } \ No newline at end of file diff --git a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java index ec74ad2..865490f 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java @@ -2,26 +2,41 @@ /** * Utility class for computing absolute turret position using two geared absolute encoders - * with the Chinese Remainder Theorem (CRT) approach. - * - * Hardware setup: + * with a CRT-like search approach. + * + * Hardware setup (example): * - Turret gear: 140 teeth - * - Encoder 13: 13-tooth gear driving an absolute encoder (reads 0–1 rev) - * - Encoder 11: 11-tooth gear driving an absolute encoder (reads 0–1 rev) - * - * The combination gives unique positions over 11 × 13 = 143 teeth → 143/140 ≈ 1.0214 turret revolutions. + * - Encoder A: e.g. 13-tooth gear driving an absolute encoder (reads 0–1 rev) + * - Encoder B: e.g. 11-tooth gear driving an absolute encoder (reads 0–1 rev) + * + * The combination gives unique positions over (encoderA_teeth × encoderB_teeth) teeth. * Beyond that range, continuity tracking (unwrapping) is used. */ public class TurretMath { - private static final double GEAR_TEETH_TURRET = 140.0; - private static final double GEAR_TEETH_ENC13 = 13.0; - private static final double GEAR_TEETH_ENC11 = 11.0; - private static final double PERIOD_TEETH = GEAR_TEETH_ENC11 * GEAR_TEETH_ENC13; // 143 - private static final double PERIOD_REV = PERIOD_TEETH / GEAR_TEETH_TURRET; // ≈1.0214 - - private static double offsetDegrees = 0; // Calibration offset (degrees) - private static double lastPositionRevs = 0.0; // Unwrapped continuous position + // Hardware tooth counts + private static final double TURRET_GEAR_TEETH = 140.0; + private static final double ENCODER_A_TEETH = 13.0; + private static final double ENCODER_B_TEETH = 11.0; + + // Derived encoder combination values + private static final double ENCODER_COMBINED_TEETH = ENCODER_A_TEETH * ENCODER_B_TEETH; // 143 (example) + private static final double ENCODER_COMBINED_PERIOD_REV = ENCODER_COMBINED_TEETH / TURRET_GEAR_TEETH; // ≈1.0214 (example) + private static final double HALF_ENCODER_COMBINED_PERIOD_REV = ENCODER_COMBINED_PERIOD_REV / 2.0; + + private static final double NORMALIZED_REV = 1.0; // encoder reading range (0..1) + private static final double HALF_NORMALIZED_REV = NORMALIZED_REV / 2.0; + private static final double DEGREES_PER_REV = 360.0; + private static final double RAD_PER_REV = 2.0 * Math.PI; + private static final double MOTOR_UNITS_PER_REV = 14.0; // scale used in degreesToMotorPosition + + // Defaults / initial values + private static final double DEFAULT_OFFSET_DEGREES = 0.0; + private static final double DEFAULT_LAST_POSITION_REVS = 0.0; + + // State + private static double offsetDegrees = DEFAULT_OFFSET_DEGREES; // Calibration offset (degrees) + private static double lastPositionRevs = DEFAULT_LAST_POSITION_REVS; // Unwrapped continuous position private static boolean initialized = false; private static double mod(double x, double m) { @@ -29,41 +44,43 @@ private static double mod(double x, double m) { } /** - * Computes the raw absolute turret position in revolutions of the 13-tooth gear - * (i.e. position in "13-gear equivalent revolutions"). - * Range is approximately 0 to (143/13) ≈ 11.0 revolutions of the 13-gear. - *

- * This uses a search-based CRT solution (most reliable for real hardware). + * Computes the raw absolute turret position in revolutions of the encoder-A gear + * (i.e. position in "encoder-A-gear-equivalent revolutions"). + * Range is approximately 0 to (combined_teeth / encoderA_teeth) revolutions of the encoder-A gear. + * + * This uses a search-based approach across the possible integer wraps of the encoder-A reading + * to find the branch that best matches the encoder-B reading. * - * @param enc13 Absolute encoder on 13-tooth gear, normalized [0, 1) - * @param enc11 Absolute encoder on 11-tooth gear, normalized [0, 1) - * @return Raw turret position in 13-gear revolutions (multiply by 13/140 for turret revs) + * @param encoderAReading Absolute encoder A reading (normalized [0, 1)) + * @param encoderBReading Absolute encoder B reading (normalized [0, 1)) + * @return Raw turret position in encoder-A-gear revolutions */ - public static double getRawPosition13Revs(double enc13, double enc11) { - double offsetRevs = offsetDegrees / 360.0; + public static double getRawPositionPrimaryRevs(double encoderAReading, double encoderBReading) { + double offsetRevs = offsetDegrees / DEGREES_PER_REV; // Apply offset and wrap to [0,1) - enc13 = mod(enc13 - offsetRevs, 1.0); - enc11 = mod(enc11 - offsetRevs, 1.0); + encoderAReading = mod(encoderAReading - offsetRevs, NORMALIZED_REV); + encoderBReading = mod(encoderBReading - offsetRevs, NORMALIZED_REV); double bestError = Double.POSITIVE_INFINITY; double bestPosition = 0.0; - // Search over the 11 possible integer steps of the slower gear - for (int k = 0; k < (int) GEAR_TEETH_ENC11; k++) { - double assumed13Revs = enc13 + k; - // Predict what enc11 *should* read if this is the correct branch - double predicted11 = mod(assumed13Revs * (GEAR_TEETH_ENC13 / GEAR_TEETH_ENC11), 1.0); + // Search over the possible integer wraps of the secondary/primary relationship + int searchCount = (int) ENCODER_B_TEETH; // number of distinct branches to check (encoder B teeth) + for (int k = 0; k < searchCount; k++) { + double assumedPrimaryRevs = encoderAReading + k; + // Predict what the encoder-B reading should be if this is the correct branch + double predictedEncoderB = mod(assumedPrimaryRevs * (ENCODER_A_TEETH / ENCODER_B_TEETH), NORMALIZED_REV); - double error = Math.abs(predicted11 - enc11); + double error = Math.abs(predictedEncoderB - encoderBReading); // Handle wrap-around distance - if (error > 0.5) { - error = 1.0 - error; + if (error > HALF_NORMALIZED_REV) { + error = NORMALIZED_REV - error; } if (error < bestError) { bestError = error; - bestPosition = assumed13Revs; + bestPosition = assumedPrimaryRevs; } } @@ -73,20 +90,20 @@ public static double getRawPosition13Revs(double enc13, double enc11) { /** * Returns the turret angle in revolutions, unwrapped for continuous multi-turn motion. * Applies offset and continuity tracking. - * - * @param enc13 Absolute encoder on 13-tooth gear [0,1) - * @param enc11 Absolute encoder on 11-tooth gear [0,1) + * + * @param encoderAReading Absolute encoder A reading [0,1) + * @param encoderBReading Absolute encoder B reading [0,1) * @return Continuous turret position in revolutions (can be >1 or <0) */ - public static double getTurretAngleRevs(double enc13, double enc11) { - double raw13Revs = getRawPosition13Revs(enc13, enc11); - double rawTurretRevs = raw13Revs * (GEAR_TEETH_ENC13 / GEAR_TEETH_TURRET); + public static double getTurretAngleRevs(double encoderAReading, double encoderBReading) { + double rawPrimaryRevs = getRawPositionPrimaryRevs(encoderAReading, encoderBReading); + double rawTurretRevs = rawPrimaryRevs * (ENCODER_A_TEETH / TURRET_GEAR_TEETH); // Apply offset again (in turret space) - rawTurretRevs -= offsetDegrees / 360.0; + rawTurretRevs -= offsetDegrees / DEGREES_PER_REV; - // Wrap raw reading into one period for comparison - double wrapped = mod(rawTurretRevs, PERIOD_REV); + // Wrap raw reading into one encoded period for comparison + double wrapped = mod(rawTurretRevs, ENCODER_COMBINED_PERIOD_REV); if (!initialized) { lastPositionRevs = wrapped; @@ -97,11 +114,11 @@ public static double getTurretAngleRevs(double enc13, double enc11) { // Compute shortest path delta (assuming small motion between calls) double delta = wrapped - lastPositionRevs; - // Unwrap using the known period - if (delta > PERIOD_REV / 2.0) { - delta -= PERIOD_REV; - } else if (delta < -PERIOD_REV / 2.0) { - delta += PERIOD_REV; + // Unwrap using the known encoder combined period + if (delta > HALF_ENCODER_COMBINED_PERIOD_REV) { + delta -= ENCODER_COMBINED_PERIOD_REV; + } else if (delta < -HALF_ENCODER_COMBINED_PERIOD_REV) { + delta += ENCODER_COMBINED_PERIOD_REV; } lastPositionRevs += delta; @@ -113,26 +130,26 @@ public static double getTurretAngleRevs(double enc13, double enc11) { * Use this when you only care about single-turn angle. */ public static double toDegreesWrapped(double turretRevs) { - return mod(turretRevs, 1.0) * 360.0; + return mod(turretRevs, NORMALIZED_REV) * DEGREES_PER_REV; } /** * Convert turret revolutions to radians, wrapped to [0, 2π). */ public static double toRadiansWrapped(double turretRevs) { - return mod(turretRevs, 1.0) * 2.0 * Math.PI; + return mod(turretRevs, NORMALIZED_REV) * RAD_PER_REV; } public static double degreesToMotorPosition(double turretDegrees) { - return (turretDegrees * 14) / 360; + return (turretDegrees * MOTOR_UNITS_PER_REV) / DEGREES_PER_REV; } public static double normalizeTurretHeading(double turretHeading, double zeroDegrees) { double newHeading = turretHeading - zeroDegrees; if (newHeading < 0) { - newHeading = 360 - Math.abs(newHeading); - } + newHeading = DEGREES_PER_REV - Math.abs(newHeading); + } return newHeading; } @@ -142,7 +159,7 @@ public static double normalizeTurretHeading(double turretHeading, double zeroDeg * Use this when you want continuous angle for PID or motion profiling. */ public static double toDegreesContinuous(double turretRevs) { - return turretRevs * 360.0; + return turretRevs * DEGREES_PER_REV; } // Calibration / zeroing @@ -154,13 +171,13 @@ public static double getOffsetDegrees() { return offsetDegrees; } - // Reset continuity tracker (call on robot enable or after large jumps) + // Reset continuity tracker public static void resetContinuity() { initialized = false; - lastPositionRevs = 0.0; + lastPositionRevs = DEFAULT_LAST_POSITION_REVS; } public static double toRad(double angle) { - return angle * (Math.PI / 180); + return angle * (Math.PI / DEGREES_PER_REV); } -} \ No newline at end of file +} diff --git a/src/main/java/frc/robot/subsystems/vision/photon/Camera.java b/src/main/java/frc/robot/subsystems/vision/photon/Camera.java index 6945af4..6f7d68f 100644 --- a/src/main/java/frc/robot/subsystems/vision/photon/Camera.java +++ b/src/main/java/frc/robot/subsystems/vision/photon/Camera.java @@ -53,12 +53,6 @@ public EstimatedRobotPose getEstimatedRobotPose() { } return bestPose.orElse(null); - - // var estimatedPose = poseEstimator.update(result).orElse(null); - - // Logger.recordOutput("EstimatedPose/" + this.id, estimatedPose.estimatedPose); - - // return estimatedPose; } public PhotonCamera getPhotonCamera() { From 95cbf55b2eaaa5b2a3dc2fca04924e86257b4b26 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 22 Feb 2026 12:49:01 -0500 Subject: [PATCH 09/19] Git ignore .idea folder, converted AngleLowPassFilter.java to a generic LowPassFilter, and added guardrails in the StateMachine --- .gitignore | 3 +- src/main/java/frc/lib/AngleLowPassFilter.java | 71 ------------------- src/main/java/frc/lib/LowPassFilter.java | 63 ++++++++++++++++ src/main/java/frc/lib/state/StateMachine.java | 40 ++++++++--- 4 files changed, 97 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/frc/lib/AngleLowPassFilter.java create mode 100644 src/main/java/frc/lib/LowPassFilter.java diff --git a/.gitignore b/.gitignore index c7cdeb2..59df83c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ hs_err_pid* replay_pid* .gradle/ -build/ \ No newline at end of file +build/ +.idea/ \ No newline at end of file diff --git a/src/main/java/frc/lib/AngleLowPassFilter.java b/src/main/java/frc/lib/AngleLowPassFilter.java deleted file mode 100644 index 7949a76..0000000 --- a/src/main/java/frc/lib/AngleLowPassFilter.java +++ /dev/null @@ -1,71 +0,0 @@ -package frc.lib; - -public class AngleLowPassFilter { - - private double filteredX; - private double filteredY; - private boolean initialized; - - private double alpha; // smoothing factor (0..1) - - /** - * @param alpha smoothing factor. - * smaller = smoother but more lag. - * recommended starting range: 0.1 - 0.2 - */ - public AngleLowPassFilter(double alpha) { - setAlpha(alpha); - this.initialized = false; - } - - /** - * Update filter with new angle measurement. - * - * @param angleDeg angle in degrees - * @return filtered angle in degrees - */ - public double calculate(double angleDeg) { - double rad = Math.toRadians(angleDeg); - - double x = Math.cos(rad); - double y = Math.sin(rad); - - if (!initialized) { - filteredX = x; - filteredY = y; - initialized = true; - } - - filteredX += alpha * (x - filteredX); - filteredY += alpha * (y - filteredY); - - return Math.toDegrees(Math.atan2(filteredY, filteredX)); - } - - /** - * Reset filter state to a known angle. - */ - public void reset(double angleDeg) { - double rad = Math.toRadians(angleDeg); - filteredX = Math.cos(rad); - filteredY = Math.sin(rad); - initialized = true; - } - - /** - * Change smoothing strength dynamically. - */ - public void setAlpha(double alpha) { - if (alpha <= 0 || alpha > 1) { - throw new IllegalArgumentException("Alpha must be in (0, 1]"); - } - this.alpha = alpha; - } - - /** - * Whether filter has received first value. - */ - public boolean isInitialized() { - return initialized; - } -} diff --git a/src/main/java/frc/lib/LowPassFilter.java b/src/main/java/frc/lib/LowPassFilter.java new file mode 100644 index 0000000..2f0c966 --- /dev/null +++ b/src/main/java/frc/lib/LowPassFilter.java @@ -0,0 +1,63 @@ +package frc.lib; + +public class LowPassFilter { + + private double filteredValue; + private boolean initialized; + private double alpha; // smoothing factor (0..1) + + /** + * @param alpha smoothing factor. + * smaller = smoother but more lag. + * recommended starting range: 0.1 - 0.2 + */ + public LowPassFilter(double alpha) { + setAlpha(alpha); + this.initialized = false; + } + + /** + * Update filter with new measurement. + * + * @param input new raw value + * @return filtered value + */ + public double calculate(double input) { + + if (!initialized) { + filteredValue = input; + initialized = true; + return filteredValue; + } + + filteredValue += alpha * (input - filteredValue); + + return filteredValue; + } + + /** + * Reset filter to a known value. + */ + public void reset(double value) { + filteredValue = value; + initialized = true; + } + + /** + * Change smoothing strength dynamically. + */ + public void setAlpha(double alpha) { + if (alpha <= 0 || alpha > 1) { + throw new IllegalArgumentException("Alpha must be in (0, 1]"); + } + this.alpha = alpha; + } + + public boolean isInitialized() { + return initialized; + } + + public double get() { + return filteredValue; + } +} \ No newline at end of file diff --git a/src/main/java/frc/lib/state/StateMachine.java b/src/main/java/frc/lib/state/StateMachine.java index f799fb8..9821d66 100644 --- a/src/main/java/frc/lib/state/StateMachine.java +++ b/src/main/java/frc/lib/state/StateMachine.java @@ -11,6 +11,10 @@ public class StateMachine > { private E currentState; private final Map> stateNodes; + // Guard fields to prevent multiple transitions within a single tick + private boolean inTick = false; + private boolean transitionFiredThisTick = false; + public StateMachine(E initialState, Map> stateNodes) { this.currentState = initialState; this.stateNodes = stateNodes; @@ -82,19 +86,39 @@ public StateConfigurer when(BooleanSupplier guard) { } public void tick() { - StateNode stateNode = stateNodes.get(currentState); - for (Transition transition : stateNode.transitions) { - if (transition.guard.getAsBoolean()) { - stateNode.onExit.run(); - currentState = transition.target; - stateNodes.get(currentState).onEnter.run(); - return; + // prevent re-entrant ticks + if (inTick) return; + + inTick = true; + transitionFiredThisTick = false; + try { + StateNode stateNode = stateNodes.get(currentState); + for (Transition transition : stateNode.transitions) { + if (transition.guard.getAsBoolean()) { + // mark that a transition has fired for this tick before running enter/exit + transitionFiredThisTick = true; + stateNode.onExit.run(); + currentState = transition.target; + stateNodes.get(currentState).onEnter.run(); + return; + } } + stateNode.onTick.run(); + } finally { + // reset guards when tick completes + inTick = false; + transitionFiredThisTick = false; } - stateNode.onTick.run(); } public void transitionTo(E targetState) { + // If called during a tick and a transition already fired, ignore to avoid cascading transitions + if (inTick && transitionFiredThisTick) { + return; + } + // If called during tick and no transition has fired yet, mark one now + if (inTick) transitionFiredThisTick = true; + StateNode stateNode = stateNodes.get(currentState); stateNode.onExit.run(); currentState = targetState; From 1abef2649395fd9a8ac97f4ce1ce04d7234a0e61 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 24 Feb 2026 13:19:33 -0500 Subject: [PATCH 10/19] Added comments --- src/main/java/frc/lib/state/StateMachine.java | 146 ------------------ .../frc/robot/subsystems/shooter/Shooter.java | 18 +++ .../robot/subsystems/shooter/ShooterIO.java | 11 ++ .../frc/robot/subsystems/turret/Turret.java | 44 +++--- .../turret/TurretIOSensorInputs.java | 12 +- .../turret/calc/ShotCompensation.java | 14 ++ .../subsystems/turret/calc/TurretMath.java | 6 + 7 files changed, 80 insertions(+), 171 deletions(-) delete mode 100644 src/main/java/frc/lib/state/StateMachine.java create mode 100644 src/main/java/frc/robot/subsystems/shooter/Shooter.java create mode 100644 src/main/java/frc/robot/subsystems/shooter/ShooterIO.java diff --git a/src/main/java/frc/lib/state/StateMachine.java b/src/main/java/frc/lib/state/StateMachine.java deleted file mode 100644 index 9821d66..0000000 --- a/src/main/java/frc/lib/state/StateMachine.java +++ /dev/null @@ -1,146 +0,0 @@ -package frc.lib.state; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; - -public class StateMachine > { - private E currentState; - private final Map> stateNodes; - - // Guard fields to prevent multiple transitions within a single tick - private boolean inTick = false; - private boolean transitionFiredThisTick = false; - - public StateMachine(E initialState, Map> stateNodes) { - this.currentState = initialState; - this.stateNodes = stateNodes; - stateNodes.get(currentState).onEnter.run(); - } - - public static > Builder forEnum() { - return new Builder<>(); - } - - private StateNode state(E e) { - StateNode def = stateNodes.get(e); - if (def == null) throw new IllegalStateException("State not defined: " + e); - return def; - } - - public static final class Builder> { - private E initial; - private final Map> states = new HashMap<>(); - - public Builder initial(E initial) { - this.initial = initial; - return this; - } - - public Builder state(E id, Consumer> cfg) { - StateNode def = states.computeIfAbsent(id, k -> new StateNode<>()); - cfg.accept(new StateConfigurer<>(def)); - return this; - } - - public StateMachine build() { - if (initial == null) throw new IllegalStateException("initial(...) is required"); - if (!states.containsKey(initial)) - throw new IllegalStateException("Initial state not defined: " + initial); - return new StateMachine<>(initial, states); - } - } - - public static final class StateConfigurer> { - private final StateNode def; - - StateConfigurer(StateNode def) { this.def = def; } - - public StateConfigurer onEnter(Runnable r) { def.onEnter = r; return this; } - public StateConfigurer onExit(Runnable r) { def.onExit = r; return this; } - public StateConfigurer onTick(Runnable r) { def.onTick = r; return this; } - - public TransitionConfigurer transitionTo(S target) { - Transition t = new Transition<>(target); - def.transitions.add(t); - return new TransitionConfigurer<>(this, t); - } - } - - public static final class TransitionConfigurer> { - private final StateConfigurer parent; - private final Transition t; - - TransitionConfigurer(StateConfigurer parent, Transition t) { - this.parent = parent; - this.t = t; - } - - public StateConfigurer when(BooleanSupplier guard) { - t.guard = guard; - return parent; // return to state configurer for chaining - } - } - - public void tick() { - // prevent re-entrant ticks - if (inTick) return; - - inTick = true; - transitionFiredThisTick = false; - try { - StateNode stateNode = stateNodes.get(currentState); - for (Transition transition : stateNode.transitions) { - if (transition.guard.getAsBoolean()) { - // mark that a transition has fired for this tick before running enter/exit - transitionFiredThisTick = true; - stateNode.onExit.run(); - currentState = transition.target; - stateNodes.get(currentState).onEnter.run(); - return; - } - } - stateNode.onTick.run(); - } finally { - // reset guards when tick completes - inTick = false; - transitionFiredThisTick = false; - } - } - - public void transitionTo(E targetState) { - // If called during a tick and a transition already fired, ignore to avoid cascading transitions - if (inTick && transitionFiredThisTick) { - return; - } - // If called during tick and no transition has fired yet, mark one now - if (inTick) transitionFiredThisTick = true; - - StateNode stateNode = stateNodes.get(currentState); - stateNode.onExit.run(); - currentState = targetState; - stateNodes.get(currentState).onEnter.run(); - } - - public E getCurrentState() { - return currentState; - } - - public static final class StateNode { - Runnable onEnter = () -> {}; - Runnable onExit = () -> {}; - Runnable onTick = () -> {}; - List> transitions = new ArrayList<>(); - } - - public static final class Transition { - final E target; - BooleanSupplier guard = () -> true; - - Transition(E target) { this.target = target; } - } - -} diff --git a/src/main/java/frc/robot/subsystems/shooter/Shooter.java b/src/main/java/frc/robot/subsystems/shooter/Shooter.java new file mode 100644 index 0000000..9884a97 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/Shooter.java @@ -0,0 +1,18 @@ +package frc.robot.subsystems.shooter; + +import frc.lib.subsystem.SpikeSystem; + +public class Shooter extends SpikeSystem { + + private final ShooterIO shooterInputs; + + public Shooter(String name, ShooterIO.ShooterIOInputs io) { + super(name, io); + } + + @Override + protected Runnable setupDataRefresher() { + shooterInputs = + return useAsyncDataRefresher(); + } +} diff --git a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java new file mode 100644 index 0000000..b4c5912 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.shooter; + +import frc.lib.subsystem.BaseIO; +import frc.lib.subsystem.BaseInputClass; + +public interface ShooterIO extends BaseIO { + + public static class ShooterIOInputs extends BaseInputClass { + + } +} diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 52fcf47..148c2aa 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -5,10 +5,8 @@ import edu.wpi.first.math.geometry.Translation2d; import frc.lib.Elastic; import frc.lib.FieldConstants; -import frc.lib.SpikeController; import frc.lib.Elastic.Notification; import frc.lib.Elastic.NotificationLevel; -import frc.lib.state.StateMachine; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; @@ -22,41 +20,39 @@ public class Turret extends SpikeSystem { public enum State { TARGETING_HUB, TARGETING_SHUTTLE } - private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + private State currentState = State.TARGETING_HUB; - private final StateMachine tsm; + private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); public void switchState(State state) { - tsm.transitionTo(state); + currentState = state; } public Turret(CommandSwerveDrivetrain drive) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; - this.tsm = StateMachine.forEnum() - .initial(State.TARGETING_HUB) - .state(State.TARGETING_HUB, cfg -> cfg - .onEnter(() -> { - Elastic.sendNotification( + } + + @Override + public void onPeriodic() { + switch (currentState) { + case TARGETING_HUB -> { + Elastic.sendNotification( new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") - ); - Elastic.selectTab("Scoring Mode"); - targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - })) - .state(State.TARGETING_SHUTTLE, cfg -> cfg.onEnter(() -> { - Elastic.sendNotification( - new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") + ); + Elastic.selectTab("Scoring Mode"); + targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + } + case TARGETING_SHUTTLE -> { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") ); Elastic.selectTab("Shuttling Mode"); targetPos = new Translation2d(); - })) - .build(); - } + } + } - @Override - public void onPeriodic() { - tsm.tick(); Logger.recordOutput("Turret/PinionEncoder", io.pinionEncoder); Logger.recordOutput("Turret/FollowerEncoder", io.followerEncoder); Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); @@ -68,7 +64,7 @@ public void onPeriodic() { drive.getPose(), drive.getState().Speeds, new Pose2d(targetPos, new Rotation2d()), - 0.3 // TODO: tune + 0.3 // see #23 ); sensorData.setTurretAngle(targetAngleCompensated.turretAngleDeg()); diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 34e3dba..9c8a774 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -75,35 +75,43 @@ public void setTurretAngle(double fieldAngleDeg) { var drive = RobotContainer.getDrive(); + // convert robot heading to [0, 360) range double robotHeadingDeg = MathUtil.inputModulus( drive.getPose().getRotation().getDegrees(), 0.0, 360.0 ); + // convert robot angular velocity to degrees per second double robotOmegaDegPerSec = drive.getState().Speeds.omegaRadiansPerSecond * 180.0 / Math.PI; + // dt = time we expect turret to reach commanded angle, tunable double dt = 0.025; double predictedHeadingDeg = robotHeadingDeg + robotOmegaDegPerSec * dt; + // turret angle in (-180, 180] range, where positive is counterclockwise from the field forward direction double turretAngleDeg = MathUtil.inputModulus( fieldAngleDeg + predictedHeadingDeg, -180.0, 180.0 ); + // convert turret angle to motor rotations, accounting for gear ratio and offset double rotations = (turretAngleDeg / 360.0) * kTurretGearRatio; rotations = -rotations + turretRotationOffset; + // calculate feedforward to counteract robot rotation, using a simple linear model with gain determined empirically double motorRps = -(robotOmegaDegPerSec / 360.0) * kTurretGearRatio; + // set the motor to the desired position with feedforward to counteract robot rotation turretMotor.setControl( mmVoltage .withPosition(rotations) + // 0.1167 is an empirically determined gain to convert from motor RPS to voltage needed to hold position against rotation .withFeedForward(motorRps * 0.1167) ); @@ -117,7 +125,8 @@ public void updateInputs(TurretIOInputs inputs) { inputs.pinionEncoder = this.pinionEncoder.get(); inputs.followerEncoder = this.followerEncoder.get(); double turretRotations = TurretMath.getTurretAngleRevs(inputs.pinionEncoder, inputs.followerEncoder); - + + // convert raw encoder readings to turret angle in degrees, accounting for gear ratio and offset inputs.turretAngleDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), turretDegreesOffset @@ -136,6 +145,7 @@ public double getAngleOffsetFromPose( Translation2d robotPose, Translation2d target ) { + // calculate angle from robot to target in field coordinates double fieldAngleDeg = Math.toDegrees( Math.atan2( target.getY() - robotPose.getY(), diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java index 1479001..f19f80e 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java @@ -10,6 +10,14 @@ public class ShotCompensation { public record AdjustedShot(double rpm, double hoodAngleDeg, double turretAngleDeg, double turretFF_deg_s, double rangeFF_m_s) {} + /** + * Calculate adjustments to shot parameters to compensate for robot movement during the shot, using a simple linear prediction of target motion based on current velocity. + * @param robotPose current robot pose on the field + * @param fieldRelVel current robot velocity in field-relative coordinates (x is forward, y is left, omega is CCW positive), used to predict where the target will be when the shot arrives + * @param targetPose current target pose on the field, used to calculate initial aiming angle and distance for compensation calculations + * @param nominalShotTimeS estimated time from shot release to target impact at the nominal shot speed, used to predict where the target will be when the shot arrives. This should be based on empirical measurements of the actual shot time for the given shot parameters, and is critical for accurate compensation. + * @return adjusted shot parameters including feedforward terms for turret angle and shot speed to compensate for robot movement, as well as the effective turret angle to aim at after compensation. The caller should apply the feedforward terms to the turret control and shot speed commands to achieve the desired compensation. + */ public static AdjustedShot compensateForMovement( Pose2d robotPose, ChassisSpeeds fieldRelVel, @@ -80,6 +88,12 @@ public static AdjustedShot compensateForMovement( turretFF_deg_s, rangeFF_m_s); } + /** + * Calculate a feedforward term to compensate for the robot's velocity toward the target, which reduces effective range and thus shot time. + * @param velocity current robot velocity in field-relative coordinates + * @param directionToTarget unit vector pointing from robot to target in field-relative coordinates + * @return feedforward term to add to shot speed (in whatever units the caller expects, legacy scaling applies) + */ private static double calculateVelocityCompensation( ChassisSpeeds velocity, Translation2d directionToTarget) { diff --git a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java index 865490f..dbd66e8 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java @@ -39,6 +39,12 @@ public class TurretMath { private static double lastPositionRevs = DEFAULT_LAST_POSITION_REVS; // Unwrapped continuous position private static boolean initialized = false; + /** + * Helper method to perform modulus that always returns a positive result, wrapping x into [0, m). + * @param x value to wrap + * @param m modulus (e.g. 1.0 for normalized encoder readings) + * @return wrapped value in [0, m) + */ private static double mod(double x, double m) { return ((x % m) + m) % m; } From 07ba2b5bd14640bd909d8ef488ba3c5595909563 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 24 Feb 2026 13:21:16 -0500 Subject: [PATCH 11/19] Revert "Added comments" This reverts commit 1abef2649395fd9a8ac97f4ce1ce04d7234a0e61. --- src/main/java/frc/lib/state/StateMachine.java | 146 ++++++++++++++++++ .../frc/robot/subsystems/shooter/Shooter.java | 18 --- .../robot/subsystems/shooter/ShooterIO.java | 11 -- .../frc/robot/subsystems/turret/Turret.java | 44 +++--- .../turret/TurretIOSensorInputs.java | 12 +- .../turret/calc/ShotCompensation.java | 14 -- .../subsystems/turret/calc/TurretMath.java | 6 - 7 files changed, 171 insertions(+), 80 deletions(-) create mode 100644 src/main/java/frc/lib/state/StateMachine.java delete mode 100644 src/main/java/frc/robot/subsystems/shooter/Shooter.java delete mode 100644 src/main/java/frc/robot/subsystems/shooter/ShooterIO.java diff --git a/src/main/java/frc/lib/state/StateMachine.java b/src/main/java/frc/lib/state/StateMachine.java new file mode 100644 index 0000000..9821d66 --- /dev/null +++ b/src/main/java/frc/lib/state/StateMachine.java @@ -0,0 +1,146 @@ +package frc.lib.state; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +public class StateMachine > { + private E currentState; + private final Map> stateNodes; + + // Guard fields to prevent multiple transitions within a single tick + private boolean inTick = false; + private boolean transitionFiredThisTick = false; + + public StateMachine(E initialState, Map> stateNodes) { + this.currentState = initialState; + this.stateNodes = stateNodes; + stateNodes.get(currentState).onEnter.run(); + } + + public static > Builder forEnum() { + return new Builder<>(); + } + + private StateNode state(E e) { + StateNode def = stateNodes.get(e); + if (def == null) throw new IllegalStateException("State not defined: " + e); + return def; + } + + public static final class Builder> { + private E initial; + private final Map> states = new HashMap<>(); + + public Builder initial(E initial) { + this.initial = initial; + return this; + } + + public Builder state(E id, Consumer> cfg) { + StateNode def = states.computeIfAbsent(id, k -> new StateNode<>()); + cfg.accept(new StateConfigurer<>(def)); + return this; + } + + public StateMachine build() { + if (initial == null) throw new IllegalStateException("initial(...) is required"); + if (!states.containsKey(initial)) + throw new IllegalStateException("Initial state not defined: " + initial); + return new StateMachine<>(initial, states); + } + } + + public static final class StateConfigurer> { + private final StateNode def; + + StateConfigurer(StateNode def) { this.def = def; } + + public StateConfigurer onEnter(Runnable r) { def.onEnter = r; return this; } + public StateConfigurer onExit(Runnable r) { def.onExit = r; return this; } + public StateConfigurer onTick(Runnable r) { def.onTick = r; return this; } + + public TransitionConfigurer transitionTo(S target) { + Transition t = new Transition<>(target); + def.transitions.add(t); + return new TransitionConfigurer<>(this, t); + } + } + + public static final class TransitionConfigurer> { + private final StateConfigurer parent; + private final Transition t; + + TransitionConfigurer(StateConfigurer parent, Transition t) { + this.parent = parent; + this.t = t; + } + + public StateConfigurer when(BooleanSupplier guard) { + t.guard = guard; + return parent; // return to state configurer for chaining + } + } + + public void tick() { + // prevent re-entrant ticks + if (inTick) return; + + inTick = true; + transitionFiredThisTick = false; + try { + StateNode stateNode = stateNodes.get(currentState); + for (Transition transition : stateNode.transitions) { + if (transition.guard.getAsBoolean()) { + // mark that a transition has fired for this tick before running enter/exit + transitionFiredThisTick = true; + stateNode.onExit.run(); + currentState = transition.target; + stateNodes.get(currentState).onEnter.run(); + return; + } + } + stateNode.onTick.run(); + } finally { + // reset guards when tick completes + inTick = false; + transitionFiredThisTick = false; + } + } + + public void transitionTo(E targetState) { + // If called during a tick and a transition already fired, ignore to avoid cascading transitions + if (inTick && transitionFiredThisTick) { + return; + } + // If called during tick and no transition has fired yet, mark one now + if (inTick) transitionFiredThisTick = true; + + StateNode stateNode = stateNodes.get(currentState); + stateNode.onExit.run(); + currentState = targetState; + stateNodes.get(currentState).onEnter.run(); + } + + public E getCurrentState() { + return currentState; + } + + public static final class StateNode { + Runnable onEnter = () -> {}; + Runnable onExit = () -> {}; + Runnable onTick = () -> {}; + List> transitions = new ArrayList<>(); + } + + public static final class Transition { + final E target; + BooleanSupplier guard = () -> true; + + Transition(E target) { this.target = target; } + } + +} diff --git a/src/main/java/frc/robot/subsystems/shooter/Shooter.java b/src/main/java/frc/robot/subsystems/shooter/Shooter.java deleted file mode 100644 index 9884a97..0000000 --- a/src/main/java/frc/robot/subsystems/shooter/Shooter.java +++ /dev/null @@ -1,18 +0,0 @@ -package frc.robot.subsystems.shooter; - -import frc.lib.subsystem.SpikeSystem; - -public class Shooter extends SpikeSystem { - - private final ShooterIO shooterInputs; - - public Shooter(String name, ShooterIO.ShooterIOInputs io) { - super(name, io); - } - - @Override - protected Runnable setupDataRefresher() { - shooterInputs = - return useAsyncDataRefresher(); - } -} diff --git a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java deleted file mode 100644 index b4c5912..0000000 --- a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java +++ /dev/null @@ -1,11 +0,0 @@ -package frc.robot.subsystems.shooter; - -import frc.lib.subsystem.BaseIO; -import frc.lib.subsystem.BaseInputClass; - -public interface ShooterIO extends BaseIO { - - public static class ShooterIOInputs extends BaseInputClass { - - } -} diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 148c2aa..52fcf47 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -5,8 +5,10 @@ import edu.wpi.first.math.geometry.Translation2d; import frc.lib.Elastic; import frc.lib.FieldConstants; +import frc.lib.SpikeController; import frc.lib.Elastic.Notification; import frc.lib.Elastic.NotificationLevel; +import frc.lib.state.StateMachine; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; @@ -20,39 +22,41 @@ public class Turret extends SpikeSystem { public enum State { TARGETING_HUB, TARGETING_SHUTTLE } - private State currentState = State.TARGETING_HUB; - private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + private final StateMachine tsm; + public void switchState(State state) { - currentState = state; + tsm.transitionTo(state); } public Turret(CommandSwerveDrivetrain drive) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; - } - - @Override - public void onPeriodic() { - switch (currentState) { - case TARGETING_HUB -> { - Elastic.sendNotification( + this.tsm = StateMachine.forEnum() + .initial(State.TARGETING_HUB) + .state(State.TARGETING_HUB, cfg -> cfg + .onEnter(() -> { + Elastic.sendNotification( new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") - ); - Elastic.selectTab("Scoring Mode"); - targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - } - case TARGETING_SHUTTLE -> { - Elastic.sendNotification( - new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") + ); + Elastic.selectTab("Scoring Mode"); + targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + })) + .state(State.TARGETING_SHUTTLE, cfg -> cfg.onEnter(() -> { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") ); Elastic.selectTab("Shuttling Mode"); targetPos = new Translation2d(); - } - } + })) + .build(); + } + @Override + public void onPeriodic() { + tsm.tick(); Logger.recordOutput("Turret/PinionEncoder", io.pinionEncoder); Logger.recordOutput("Turret/FollowerEncoder", io.followerEncoder); Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); @@ -64,7 +68,7 @@ public void onPeriodic() { drive.getPose(), drive.getState().Speeds, new Pose2d(targetPos, new Rotation2d()), - 0.3 // see #23 + 0.3 // TODO: tune ); sensorData.setTurretAngle(targetAngleCompensated.turretAngleDeg()); diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 9c8a774..34e3dba 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -75,43 +75,35 @@ public void setTurretAngle(double fieldAngleDeg) { var drive = RobotContainer.getDrive(); - // convert robot heading to [0, 360) range double robotHeadingDeg = MathUtil.inputModulus( drive.getPose().getRotation().getDegrees(), 0.0, 360.0 ); - // convert robot angular velocity to degrees per second double robotOmegaDegPerSec = drive.getState().Speeds.omegaRadiansPerSecond * 180.0 / Math.PI; - // dt = time we expect turret to reach commanded angle, tunable double dt = 0.025; double predictedHeadingDeg = robotHeadingDeg + robotOmegaDegPerSec * dt; - // turret angle in (-180, 180] range, where positive is counterclockwise from the field forward direction double turretAngleDeg = MathUtil.inputModulus( fieldAngleDeg + predictedHeadingDeg, -180.0, 180.0 ); - // convert turret angle to motor rotations, accounting for gear ratio and offset double rotations = (turretAngleDeg / 360.0) * kTurretGearRatio; rotations = -rotations + turretRotationOffset; - // calculate feedforward to counteract robot rotation, using a simple linear model with gain determined empirically double motorRps = -(robotOmegaDegPerSec / 360.0) * kTurretGearRatio; - // set the motor to the desired position with feedforward to counteract robot rotation turretMotor.setControl( mmVoltage .withPosition(rotations) - // 0.1167 is an empirically determined gain to convert from motor RPS to voltage needed to hold position against rotation .withFeedForward(motorRps * 0.1167) ); @@ -125,8 +117,7 @@ public void updateInputs(TurretIOInputs inputs) { inputs.pinionEncoder = this.pinionEncoder.get(); inputs.followerEncoder = this.followerEncoder.get(); double turretRotations = TurretMath.getTurretAngleRevs(inputs.pinionEncoder, inputs.followerEncoder); - - // convert raw encoder readings to turret angle in degrees, accounting for gear ratio and offset + inputs.turretAngleDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), turretDegreesOffset @@ -145,7 +136,6 @@ public double getAngleOffsetFromPose( Translation2d robotPose, Translation2d target ) { - // calculate angle from robot to target in field coordinates double fieldAngleDeg = Math.toDegrees( Math.atan2( target.getY() - robotPose.getY(), diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java index f19f80e..1479001 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java @@ -10,14 +10,6 @@ public class ShotCompensation { public record AdjustedShot(double rpm, double hoodAngleDeg, double turretAngleDeg, double turretFF_deg_s, double rangeFF_m_s) {} - /** - * Calculate adjustments to shot parameters to compensate for robot movement during the shot, using a simple linear prediction of target motion based on current velocity. - * @param robotPose current robot pose on the field - * @param fieldRelVel current robot velocity in field-relative coordinates (x is forward, y is left, omega is CCW positive), used to predict where the target will be when the shot arrives - * @param targetPose current target pose on the field, used to calculate initial aiming angle and distance for compensation calculations - * @param nominalShotTimeS estimated time from shot release to target impact at the nominal shot speed, used to predict where the target will be when the shot arrives. This should be based on empirical measurements of the actual shot time for the given shot parameters, and is critical for accurate compensation. - * @return adjusted shot parameters including feedforward terms for turret angle and shot speed to compensate for robot movement, as well as the effective turret angle to aim at after compensation. The caller should apply the feedforward terms to the turret control and shot speed commands to achieve the desired compensation. - */ public static AdjustedShot compensateForMovement( Pose2d robotPose, ChassisSpeeds fieldRelVel, @@ -88,12 +80,6 @@ public static AdjustedShot compensateForMovement( turretFF_deg_s, rangeFF_m_s); } - /** - * Calculate a feedforward term to compensate for the robot's velocity toward the target, which reduces effective range and thus shot time. - * @param velocity current robot velocity in field-relative coordinates - * @param directionToTarget unit vector pointing from robot to target in field-relative coordinates - * @return feedforward term to add to shot speed (in whatever units the caller expects, legacy scaling applies) - */ private static double calculateVelocityCompensation( ChassisSpeeds velocity, Translation2d directionToTarget) { diff --git a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java index dbd66e8..865490f 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java @@ -39,12 +39,6 @@ public class TurretMath { private static double lastPositionRevs = DEFAULT_LAST_POSITION_REVS; // Unwrapped continuous position private static boolean initialized = false; - /** - * Helper method to perform modulus that always returns a positive result, wrapping x into [0, m). - * @param x value to wrap - * @param m modulus (e.g. 1.0 for normalized encoder readings) - * @return wrapped value in [0, m) - */ private static double mod(double x, double m) { return ((x % m) + m) % m; } From 421f6c3ef7c681ab574b36e57fc3582eebf794e6 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 24 Feb 2026 13:22:20 -0500 Subject: [PATCH 12/19] Reapply "Added comments" This reverts commit 07ba2b5bd14640bd909d8ef488ba3c5595909563. --- src/main/java/frc/lib/state/StateMachine.java | 146 ------------------ .../frc/robot/subsystems/shooter/Shooter.java | 18 +++ .../robot/subsystems/shooter/ShooterIO.java | 11 ++ .../frc/robot/subsystems/turret/Turret.java | 44 +++--- .../turret/TurretIOSensorInputs.java | 12 +- .../turret/calc/ShotCompensation.java | 14 ++ .../subsystems/turret/calc/TurretMath.java | 6 + 7 files changed, 80 insertions(+), 171 deletions(-) delete mode 100644 src/main/java/frc/lib/state/StateMachine.java create mode 100644 src/main/java/frc/robot/subsystems/shooter/Shooter.java create mode 100644 src/main/java/frc/robot/subsystems/shooter/ShooterIO.java diff --git a/src/main/java/frc/lib/state/StateMachine.java b/src/main/java/frc/lib/state/StateMachine.java deleted file mode 100644 index 9821d66..0000000 --- a/src/main/java/frc/lib/state/StateMachine.java +++ /dev/null @@ -1,146 +0,0 @@ -package frc.lib.state; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; - -public class StateMachine > { - private E currentState; - private final Map> stateNodes; - - // Guard fields to prevent multiple transitions within a single tick - private boolean inTick = false; - private boolean transitionFiredThisTick = false; - - public StateMachine(E initialState, Map> stateNodes) { - this.currentState = initialState; - this.stateNodes = stateNodes; - stateNodes.get(currentState).onEnter.run(); - } - - public static > Builder forEnum() { - return new Builder<>(); - } - - private StateNode state(E e) { - StateNode def = stateNodes.get(e); - if (def == null) throw new IllegalStateException("State not defined: " + e); - return def; - } - - public static final class Builder> { - private E initial; - private final Map> states = new HashMap<>(); - - public Builder initial(E initial) { - this.initial = initial; - return this; - } - - public Builder state(E id, Consumer> cfg) { - StateNode def = states.computeIfAbsent(id, k -> new StateNode<>()); - cfg.accept(new StateConfigurer<>(def)); - return this; - } - - public StateMachine build() { - if (initial == null) throw new IllegalStateException("initial(...) is required"); - if (!states.containsKey(initial)) - throw new IllegalStateException("Initial state not defined: " + initial); - return new StateMachine<>(initial, states); - } - } - - public static final class StateConfigurer> { - private final StateNode def; - - StateConfigurer(StateNode def) { this.def = def; } - - public StateConfigurer onEnter(Runnable r) { def.onEnter = r; return this; } - public StateConfigurer onExit(Runnable r) { def.onExit = r; return this; } - public StateConfigurer onTick(Runnable r) { def.onTick = r; return this; } - - public TransitionConfigurer transitionTo(S target) { - Transition t = new Transition<>(target); - def.transitions.add(t); - return new TransitionConfigurer<>(this, t); - } - } - - public static final class TransitionConfigurer> { - private final StateConfigurer parent; - private final Transition t; - - TransitionConfigurer(StateConfigurer parent, Transition t) { - this.parent = parent; - this.t = t; - } - - public StateConfigurer when(BooleanSupplier guard) { - t.guard = guard; - return parent; // return to state configurer for chaining - } - } - - public void tick() { - // prevent re-entrant ticks - if (inTick) return; - - inTick = true; - transitionFiredThisTick = false; - try { - StateNode stateNode = stateNodes.get(currentState); - for (Transition transition : stateNode.transitions) { - if (transition.guard.getAsBoolean()) { - // mark that a transition has fired for this tick before running enter/exit - transitionFiredThisTick = true; - stateNode.onExit.run(); - currentState = transition.target; - stateNodes.get(currentState).onEnter.run(); - return; - } - } - stateNode.onTick.run(); - } finally { - // reset guards when tick completes - inTick = false; - transitionFiredThisTick = false; - } - } - - public void transitionTo(E targetState) { - // If called during a tick and a transition already fired, ignore to avoid cascading transitions - if (inTick && transitionFiredThisTick) { - return; - } - // If called during tick and no transition has fired yet, mark one now - if (inTick) transitionFiredThisTick = true; - - StateNode stateNode = stateNodes.get(currentState); - stateNode.onExit.run(); - currentState = targetState; - stateNodes.get(currentState).onEnter.run(); - } - - public E getCurrentState() { - return currentState; - } - - public static final class StateNode { - Runnable onEnter = () -> {}; - Runnable onExit = () -> {}; - Runnable onTick = () -> {}; - List> transitions = new ArrayList<>(); - } - - public static final class Transition { - final E target; - BooleanSupplier guard = () -> true; - - Transition(E target) { this.target = target; } - } - -} diff --git a/src/main/java/frc/robot/subsystems/shooter/Shooter.java b/src/main/java/frc/robot/subsystems/shooter/Shooter.java new file mode 100644 index 0000000..9884a97 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/Shooter.java @@ -0,0 +1,18 @@ +package frc.robot.subsystems.shooter; + +import frc.lib.subsystem.SpikeSystem; + +public class Shooter extends SpikeSystem { + + private final ShooterIO shooterInputs; + + public Shooter(String name, ShooterIO.ShooterIOInputs io) { + super(name, io); + } + + @Override + protected Runnable setupDataRefresher() { + shooterInputs = + return useAsyncDataRefresher(); + } +} diff --git a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java new file mode 100644 index 0000000..b4c5912 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java @@ -0,0 +1,11 @@ +package frc.robot.subsystems.shooter; + +import frc.lib.subsystem.BaseIO; +import frc.lib.subsystem.BaseInputClass; + +public interface ShooterIO extends BaseIO { + + public static class ShooterIOInputs extends BaseInputClass { + + } +} diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 52fcf47..148c2aa 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -5,10 +5,8 @@ import edu.wpi.first.math.geometry.Translation2d; import frc.lib.Elastic; import frc.lib.FieldConstants; -import frc.lib.SpikeController; import frc.lib.Elastic.Notification; import frc.lib.Elastic.NotificationLevel; -import frc.lib.state.StateMachine; import frc.lib.subsystem.SpikeSystem; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; @@ -22,41 +20,39 @@ public class Turret extends SpikeSystem { public enum State { TARGETING_HUB, TARGETING_SHUTTLE } - private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + private State currentState = State.TARGETING_HUB; - private final StateMachine tsm; + private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); public void switchState(State state) { - tsm.transitionTo(state); + currentState = state; } public Turret(CommandSwerveDrivetrain drive) { super("Turret", new TurretIO.TurretIOInputs()); Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); this.drive = drive; - this.tsm = StateMachine.forEnum() - .initial(State.TARGETING_HUB) - .state(State.TARGETING_HUB, cfg -> cfg - .onEnter(() -> { - Elastic.sendNotification( + } + + @Override + public void onPeriodic() { + switch (currentState) { + case TARGETING_HUB -> { + Elastic.sendNotification( new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") - ); - Elastic.selectTab("Scoring Mode"); - targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - })) - .state(State.TARGETING_SHUTTLE, cfg -> cfg.onEnter(() -> { - Elastic.sendNotification( - new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") + ); + Elastic.selectTab("Scoring Mode"); + targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + } + case TARGETING_SHUTTLE -> { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") ); Elastic.selectTab("Shuttling Mode"); targetPos = new Translation2d(); - })) - .build(); - } + } + } - @Override - public void onPeriodic() { - tsm.tick(); Logger.recordOutput("Turret/PinionEncoder", io.pinionEncoder); Logger.recordOutput("Turret/FollowerEncoder", io.followerEncoder); Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); @@ -68,7 +64,7 @@ public void onPeriodic() { drive.getPose(), drive.getState().Speeds, new Pose2d(targetPos, new Rotation2d()), - 0.3 // TODO: tune + 0.3 // see #23 ); sensorData.setTurretAngle(targetAngleCompensated.turretAngleDeg()); diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 34e3dba..9c8a774 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -75,35 +75,43 @@ public void setTurretAngle(double fieldAngleDeg) { var drive = RobotContainer.getDrive(); + // convert robot heading to [0, 360) range double robotHeadingDeg = MathUtil.inputModulus( drive.getPose().getRotation().getDegrees(), 0.0, 360.0 ); + // convert robot angular velocity to degrees per second double robotOmegaDegPerSec = drive.getState().Speeds.omegaRadiansPerSecond * 180.0 / Math.PI; + // dt = time we expect turret to reach commanded angle, tunable double dt = 0.025; double predictedHeadingDeg = robotHeadingDeg + robotOmegaDegPerSec * dt; + // turret angle in (-180, 180] range, where positive is counterclockwise from the field forward direction double turretAngleDeg = MathUtil.inputModulus( fieldAngleDeg + predictedHeadingDeg, -180.0, 180.0 ); + // convert turret angle to motor rotations, accounting for gear ratio and offset double rotations = (turretAngleDeg / 360.0) * kTurretGearRatio; rotations = -rotations + turretRotationOffset; + // calculate feedforward to counteract robot rotation, using a simple linear model with gain determined empirically double motorRps = -(robotOmegaDegPerSec / 360.0) * kTurretGearRatio; + // set the motor to the desired position with feedforward to counteract robot rotation turretMotor.setControl( mmVoltage .withPosition(rotations) + // 0.1167 is an empirically determined gain to convert from motor RPS to voltage needed to hold position against rotation .withFeedForward(motorRps * 0.1167) ); @@ -117,7 +125,8 @@ public void updateInputs(TurretIOInputs inputs) { inputs.pinionEncoder = this.pinionEncoder.get(); inputs.followerEncoder = this.followerEncoder.get(); double turretRotations = TurretMath.getTurretAngleRevs(inputs.pinionEncoder, inputs.followerEncoder); - + + // convert raw encoder readings to turret angle in degrees, accounting for gear ratio and offset inputs.turretAngleDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), turretDegreesOffset @@ -136,6 +145,7 @@ public double getAngleOffsetFromPose( Translation2d robotPose, Translation2d target ) { + // calculate angle from robot to target in field coordinates double fieldAngleDeg = Math.toDegrees( Math.atan2( target.getY() - robotPose.getY(), diff --git a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java index 1479001..f19f80e 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/ShotCompensation.java @@ -10,6 +10,14 @@ public class ShotCompensation { public record AdjustedShot(double rpm, double hoodAngleDeg, double turretAngleDeg, double turretFF_deg_s, double rangeFF_m_s) {} + /** + * Calculate adjustments to shot parameters to compensate for robot movement during the shot, using a simple linear prediction of target motion based on current velocity. + * @param robotPose current robot pose on the field + * @param fieldRelVel current robot velocity in field-relative coordinates (x is forward, y is left, omega is CCW positive), used to predict where the target will be when the shot arrives + * @param targetPose current target pose on the field, used to calculate initial aiming angle and distance for compensation calculations + * @param nominalShotTimeS estimated time from shot release to target impact at the nominal shot speed, used to predict where the target will be when the shot arrives. This should be based on empirical measurements of the actual shot time for the given shot parameters, and is critical for accurate compensation. + * @return adjusted shot parameters including feedforward terms for turret angle and shot speed to compensate for robot movement, as well as the effective turret angle to aim at after compensation. The caller should apply the feedforward terms to the turret control and shot speed commands to achieve the desired compensation. + */ public static AdjustedShot compensateForMovement( Pose2d robotPose, ChassisSpeeds fieldRelVel, @@ -80,6 +88,12 @@ public static AdjustedShot compensateForMovement( turretFF_deg_s, rangeFF_m_s); } + /** + * Calculate a feedforward term to compensate for the robot's velocity toward the target, which reduces effective range and thus shot time. + * @param velocity current robot velocity in field-relative coordinates + * @param directionToTarget unit vector pointing from robot to target in field-relative coordinates + * @return feedforward term to add to shot speed (in whatever units the caller expects, legacy scaling applies) + */ private static double calculateVelocityCompensation( ChassisSpeeds velocity, Translation2d directionToTarget) { diff --git a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java index 865490f..dbd66e8 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java @@ -39,6 +39,12 @@ public class TurretMath { private static double lastPositionRevs = DEFAULT_LAST_POSITION_REVS; // Unwrapped continuous position private static boolean initialized = false; + /** + * Helper method to perform modulus that always returns a positive result, wrapping x into [0, m). + * @param x value to wrap + * @param m modulus (e.g. 1.0 for normalized encoder readings) + * @return wrapped value in [0, m) + */ private static double mod(double x, double m) { return ((x % m) + m) % m; } From 9b9a1e66136ca6a774ab8f678ebb3d6b3416e604 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 24 Feb 2026 13:23:22 -0500 Subject: [PATCH 13/19] Removed shooter code (on wrong branch) --- .../frc/robot/subsystems/shooter/Shooter.java | 18 ------------------ .../robot/subsystems/shooter/ShooterIO.java | 11 ----------- 2 files changed, 29 deletions(-) delete mode 100644 src/main/java/frc/robot/subsystems/shooter/Shooter.java delete mode 100644 src/main/java/frc/robot/subsystems/shooter/ShooterIO.java diff --git a/src/main/java/frc/robot/subsystems/shooter/Shooter.java b/src/main/java/frc/robot/subsystems/shooter/Shooter.java deleted file mode 100644 index 9884a97..0000000 --- a/src/main/java/frc/robot/subsystems/shooter/Shooter.java +++ /dev/null @@ -1,18 +0,0 @@ -package frc.robot.subsystems.shooter; - -import frc.lib.subsystem.SpikeSystem; - -public class Shooter extends SpikeSystem { - - private final ShooterIO shooterInputs; - - public Shooter(String name, ShooterIO.ShooterIOInputs io) { - super(name, io); - } - - @Override - protected Runnable setupDataRefresher() { - shooterInputs = - return useAsyncDataRefresher(); - } -} diff --git a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java deleted file mode 100644 index b4c5912..0000000 --- a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java +++ /dev/null @@ -1,11 +0,0 @@ -package frc.robot.subsystems.shooter; - -import frc.lib.subsystem.BaseIO; -import frc.lib.subsystem.BaseInputClass; - -public interface ShooterIO extends BaseIO { - - public static class ShooterIOInputs extends BaseInputClass { - - } -} From 42507d70c99419bd7a43fa89d1c85fdc2402042b Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 24 Feb 2026 13:28:18 -0500 Subject: [PATCH 14/19] reapply old PID values --- .../frc/robot/subsystems/turret/TurretIOSensorInputs.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java index 9c8a774..2b37137 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java @@ -42,6 +42,14 @@ public TurretIOSensorInputs() { mm.MotionMagicCruiseVelocity = 30; mm.MotionMagicJerk = 0; + Slot0Configs config = new Slot0Configs(); + config.kP = 1; + config.kI = 0.01; + config.kD = 0.3; + config.kS = 0.194; + config.kV = 0.1167; + + this.turretMotor.getConfigurator().apply(config); this.turretMotor.getConfigurator().apply(mm); this.pinionEncoder = new DutyCycleEncoder(0); this.followerEncoder = new DutyCycleEncoder(1); From 849d11fcbbf657a674bb3c3e54047231b2166896 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 24 Feb 2026 14:21:02 -0500 Subject: [PATCH 15/19] Updated comments, cleaned up code, refactored state system. --- .../drive/CommandSwerveDrivetrain.java | 9 ++ .../frc/robot/subsystems/turret/Turret.java | 63 ++++++------ .../frc/robot/subsystems/turret/TurretIO.java | 10 +- ...SensorInputs.java => TurretIOTalonFX.java} | 96 +++++++------------ 4 files changed, 81 insertions(+), 97 deletions(-) rename src/main/java/frc/robot/subsystems/turret/{TurretIOSensorInputs.java => TurretIOTalonFX.java} (62%) diff --git a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java index 30a2fbf..41ad6f8 100644 --- a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java +++ b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java @@ -55,6 +55,8 @@ public class CommandSwerveDrivetrain extends TunerSwerveDrivetrain implements Su private final SwerveRequest.SysIdSwerveSteerGains m_steerCharacterization = new SwerveRequest.SysIdSwerveSteerGains(); private final SwerveRequest.SysIdSwerveRotation m_rotationCharacterization = new SwerveRequest.SysIdSwerveRotation(); + private double robotOmegaDegPerSec = 0.0; + /* SysId routine for characterizing translation. This is used to find PID gains for the drive motors. */ private final SysIdRoutine m_sysIdRoutineTranslation = new SysIdRoutine( new SysIdRoutine.Config( @@ -250,6 +252,9 @@ public void periodic() { m_hasAppliedOperatorPerspective = true; }); } + + this.robotOmegaDegPerSec = this.getState().Speeds.omegaRadiansPerSecond + * 180.0 / Math.PI; } private void startSimThread() { @@ -349,4 +354,8 @@ public Rotation2d getRotation() { public SendableChooser getAutoChooser() { return AutoBuilder.buildAutoChooser(); } + + public double getRobotOmegaDegPerSec() { + return robotOmegaDegPerSec; + } } diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 148c2aa..1cd05ec 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -8,14 +8,14 @@ import frc.lib.Elastic.Notification; import frc.lib.Elastic.NotificationLevel; import frc.lib.subsystem.SpikeSystem; +import frc.robot.RobotContainer; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; import frc.robot.subsystems.turret.calc.ShotCompensation; -import frc.robot.subsystems.turret.calc.TurretMath; import org.littletonrobotics.junction.Logger; public class Turret extends SpikeSystem { - private TurretIOSensorInputs sensorData; + private TurretIOTalonFX turretIO; private final CommandSwerveDrivetrain drive; public enum State { TARGETING_HUB, TARGETING_SHUTTLE } @@ -24,8 +24,26 @@ public enum State { TARGETING_HUB, TARGETING_SHUTTLE } private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - public void switchState(State state) { - currentState = state; + public void setTargetingHub() { + if (currentState != State.TARGETING_HUB) { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") + ); + Elastic.selectTab("Scoring Mode"); + targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + currentState = State.TARGETING_HUB; + } + } + + public void setTargetingShuttle() { + if (currentState != State.TARGETING_SHUTTLE) { + Elastic.sendNotification( + new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") + ); + Elastic.selectTab("Shuttling Mode"); + targetPos = new Translation2d(0, 0); + currentState = State.TARGETING_SHUTTLE; + } } public Turret(CommandSwerveDrivetrain drive) { @@ -36,44 +54,21 @@ public Turret(CommandSwerveDrivetrain drive) { @Override public void onPeriodic() { - switch (currentState) { - case TARGETING_HUB -> { - Elastic.sendNotification( - new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SCORING mode") - ); - Elastic.selectTab("Scoring Mode"); - targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); - } - case TARGETING_SHUTTLE -> { - Elastic.sendNotification( - new Notification(NotificationLevel.INFO, "Switched Modes", "Switched modes to SHUTTLING mode") - ); - Elastic.selectTab("Shuttling Mode"); - targetPos = new Translation2d(); - } - } - - Logger.recordOutput("Turret/PinionEncoder", io.pinionEncoder); - Logger.recordOutput("Turret/FollowerEncoder", io.followerEncoder); - Logger.recordOutput("Turret/Angle", io.turretAngleDegrees); - double turretFieldAngleDeg = io.turretAngleDegrees; - Pose2d turretPose = new Pose2d(drive.getPose().getTranslation(), new Rotation2d(TurretMath.toRad(turretFieldAngleDeg))); - Logger.recordOutput("Turret/TurretPose", turretPose); - + // compensate for robot movement ShotCompensation.AdjustedShot targetAngleCompensated = ShotCompensation.compensateForMovement( drive.getPose(), drive.getState().Speeds, - new Pose2d(targetPos, new Rotation2d()), - 0.3 // see #23 + new Pose2d(this.targetPos, new Rotation2d()), + 0.3 // see github issue #23 (https://github.com/Team293/Rebuilt/issues/23) ); - sensorData.setTurretAngle(targetAngleCompensated.turretAngleDeg()); - Logger.recordOutput("Turret/TargetOffset", targetAngleCompensated.turretAngleDeg()); + // set the turret angle to the compensated angle + this.turretIO.setTurretAngle(targetAngleCompensated.turretAngleDeg()); } @Override protected Runnable setupDataRefresher() { - sensorData = new TurretIOSensorInputs(); - return useAsyncDataRefresher(sensorData); + this.turretIO = new TurretIOTalonFX(RobotContainer.getDrive()); + return useAsyncDataRefresher(this.turretIO); } } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIO.java b/src/main/java/frc/robot/subsystems/turret/TurretIO.java index 8d7234e..3074ba8 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIO.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIO.java @@ -1,18 +1,22 @@ package frc.robot.subsystems.turret; -import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Pose2d; import frc.lib.subsystem.BaseIO; import frc.lib.subsystem.BaseInputClass; +import frc.lib.subsystem.IORefresher; import org.littletonrobotics.junction.AutoLog; -public interface TurretIO extends BaseIO { +public interface TurretIO extends BaseIO, IORefresher { @AutoLog public static class TurretIOInputs extends BaseInputClass { public double turretAngleDegrees = 0.0; public double pinionEncoder = 0.0; public double followerEncoder = 0.0; + public double turretSetPointDegrees = 0.0; + public Pose2d turretPosition = new Pose2d(); + public double robotOmegaDegPerSec = 0.0; } - double getAngleOffsetFromPose(Translation2d robotPose, Translation2d target); + void setTurretAngle(double angle); } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java similarity index 62% rename from src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java rename to src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java index 2b37137..cc38b70 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOSensorInputs.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java @@ -7,40 +7,40 @@ import com.ctre.phoenix6.controls.MotionMagicVoltage; import com.ctre.phoenix6.hardware.TalonFX; import edu.wpi.first.math.MathUtil; -import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.units.measure.Angle; import edu.wpi.first.wpilibj.DutyCycleEncoder; -import frc.lib.subsystem.IORefresher; import frc.robot.CanID; import frc.robot.RobotContainer; +import frc.robot.subsystems.drive.CommandSwerveDrivetrain; import frc.robot.subsystems.turret.calc.TurretMath; -import frc.robot.subsystems.vision.photon.Camera; import org.littletonrobotics.junction.Logger; -import org.photonvision.targeting.PhotonPipelineResult; -import java.util.List; -import java.util.Optional; - -public class TurretIOSensorInputs implements TurretIO, IORefresher { +public class TurretIOTalonFX implements TurretIO { private final TalonFX turretMotor; private final DutyCycleEncoder pinionEncoder; private final DutyCycleEncoder followerEncoder; + private final CommandSwerveDrivetrain drive; private final MotionMagicVoltage mmVoltage = new MotionMagicVoltage(0); private final StatusSignal encoderSignal; - private static final double kTurretGearRatio = 140/10; - private final double turretRotationOffset; - private final double turretDegreesOffset = 246.7; + private static final double kTurretGearRatio = 140/10; // turret ring: 140 teeth, motor pinion: 10 teeth + + private final double turretRotationOffset; // offset in motor rotations, calculated at startup, units in motor rotations + private final double turretDegreesOffset = 246.7; // offset in degrees to align turret with front of robot + + private double targetAngleDeg = 0.0; // target angle of the turret in degrees - public TurretIOSensorInputs() { + public TurretIOTalonFX(CommandSwerveDrivetrain drive) { + this.drive = drive; this.turretMotor = new TalonFX(CanID.TURRET_MOTOR.getID()); MotionMagicConfigs mm = new MotionMagicConfigs(); - mm.MotionMagicAcceleration = 60; - mm.MotionMagicCruiseVelocity = 30; - mm.MotionMagicJerk = 0; + mm.MotionMagicAcceleration = 60; // rot/sec^2 + mm.MotionMagicCruiseVelocity = 30; // rot/sec Slot0Configs config = new Slot0Configs(); config.kP = 1; @@ -54,35 +54,30 @@ public TurretIOSensorInputs() { this.pinionEncoder = new DutyCycleEncoder(0); this.followerEncoder = new DutyCycleEncoder(1); - this.encoderSignal = turretMotor.getPosition(); + this.encoderSignal = this.turretMotor.getPosition(); - double pinionEncoderValue = pinionEncoder.get(); - double followerEncoderValue = followerEncoder.get(); + double pinionEncoderValue = this.pinionEncoder.get(); + double followerEncoderValue = this.followerEncoder.get(); // set offset of the turret on startup double turretRotations = TurretMath.getTurretAngleRevs(pinionEncoderValue, followerEncoderValue); double turretDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), - turretDegreesOffset + this.turretDegreesOffset ); - Logger.recordOutput("Turret/InitialHeading", turretDegrees); - - double currentMotorRevs = turretMotor.getPosition().getValueAsDouble(); + double currentMotorRevs = this.turretMotor.getPosition().getValueAsDouble(); double toZeroRevs = TurretMath.degreesToMotorPosition(turretDegrees); - turretRotationOffset = currentMotorRevs + toZeroRevs; - - Logger.recordOutput("Turret/ToZeroRevs", toZeroRevs); - Logger.recordOutput("Turret/TurretRotationOffset", turretRotationOffset); - } + this.turretRotationOffset = currentMotorRevs + toZeroRevs; + } + @Override public void setTurretAngle(double fieldAngleDeg) { + this.targetAngleDeg = fieldAngleDeg; fieldAngleDeg = MathUtil.inputModulus(fieldAngleDeg, 0.0, 360.0); - var drive = RobotContainer.getDrive(); - // convert robot heading to [0, 360) range double robotHeadingDeg = MathUtil.inputModulus( @@ -90,15 +85,10 @@ public void setTurretAngle(double fieldAngleDeg) { 0.0, 360.0 ); - // convert robot angular velocity to degrees per second - double robotOmegaDegPerSec = - drive.getState().Speeds.omegaRadiansPerSecond - * 180.0 / Math.PI; - // dt = time we expect turret to reach commanded angle, tunable double dt = 0.025; double predictedHeadingDeg = - robotHeadingDeg + robotOmegaDegPerSec * dt; + robotHeadingDeg + this.drive.getRobotOmegaDegPerSec() * dt; // turret angle in (-180, 180] range, where positive is counterclockwise from the field forward direction double turretAngleDeg = @@ -109,57 +99,43 @@ public void setTurretAngle(double fieldAngleDeg) { // convert turret angle to motor rotations, accounting for gear ratio and offset double rotations = (turretAngleDeg / 360.0) * kTurretGearRatio; - rotations = -rotations + turretRotationOffset; + rotations = -rotations + this.turretRotationOffset; // calculate feedforward to counteract robot rotation, using a simple linear model with gain determined empirically double motorRps = - -(robotOmegaDegPerSec / 360.0) * kTurretGearRatio; + -(this.drive.getRobotOmegaDegPerSec() / 360.0) * kTurretGearRatio; // set the motor to the desired position with feedforward to counteract robot rotation - turretMotor.setControl( - mmVoltage + this.turretMotor.setControl( + this.mmVoltage .withPosition(rotations) // 0.1167 is an empirically determined gain to convert from motor RPS to voltage needed to hold position against rotation .withFeedForward(motorRps * 0.1167) ); - - Logger.recordOutput("Turret/CommandedRotations", rotations); - - Logger.recordOutput("Turret/RobotOmegaDegPerSec", robotOmegaDegPerSec); } @Override public void updateInputs(TurretIOInputs inputs) { inputs.pinionEncoder = this.pinionEncoder.get(); inputs.followerEncoder = this.followerEncoder.get(); + inputs.turretSetPointDegrees = this.targetAngleDeg; + double turretRotations = TurretMath.getTurretAngleRevs(inputs.pinionEncoder, inputs.followerEncoder); + inputs.robotOmegaDegPerSec = this.drive.getRobotOmegaDegPerSec(); + // convert raw encoder readings to turret angle in degrees, accounting for gear ratio and offset inputs.turretAngleDegrees = TurretMath.normalizeTurretHeading( TurretMath.toDegreesWrapped(turretRotations), turretDegreesOffset ); - - Logger.recordOutput("Turret/CRTRevs", turretRotations); + + // creates a position for the turret based on robot position, rotated by turret angle for logging + inputs.turretPosition = new Pose2d(drive.getPose().getTranslation(), new Rotation2d(TurretMath.toRad(inputs.turretAngleDegrees))); } @Override public void refreshData() { BaseStatusSignal.refreshAll(encoderSignal); } - - @Override - public double getAngleOffsetFromPose( - Translation2d robotPose, - Translation2d target - ) { - // calculate angle from robot to target in field coordinates - double fieldAngleDeg = Math.toDegrees( - Math.atan2( - target.getY() - robotPose.getY(), - target.getX() - robotPose.getX() - ) - ); - return MathUtil.inputModulus(-fieldAngleDeg, -180, 180); - } } From 22354c7126f3fbaf63ae28c739f6a3cf6031fdd7 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 25 Feb 2026 12:43:31 -0500 Subject: [PATCH 16/19] Add turret cancoders and update encoder handling in TurretIOTalonFX --- src/main/java/frc/robot/CanID.java | 4 ++- .../subsystems/turret/TurretIOTalonFX.java | 25 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/frc/robot/CanID.java b/src/main/java/frc/robot/CanID.java index 14b5cf7..c019df5 100644 --- a/src/main/java/frc/robot/CanID.java +++ b/src/main/java/frc/robot/CanID.java @@ -7,7 +7,9 @@ public enum CanID { TURRET_MOTOR(13), INTAKE_MOTOR(9), INTAKE_DEPLOY_MOTOR(11), - INDEXER(10); + INDEXER(10), + TURRET_PINION_CANCODER(12), + TURRET_FOLLOWER_CANCODER(14); private final int deviceID; diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java index cc38b70..89679f7 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java @@ -2,9 +2,12 @@ import com.ctre.phoenix6.BaseStatusSignal; import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.MagnetSensorConfigs; import com.ctre.phoenix6.configs.MotionMagicConfigs; import com.ctre.phoenix6.configs.Slot0Configs; import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.hardware.CANcoder; import com.ctre.phoenix6.hardware.TalonFX; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Pose2d; @@ -20,12 +23,12 @@ public class TurretIOTalonFX implements TurretIO { private final TalonFX turretMotor; - private final DutyCycleEncoder pinionEncoder; - private final DutyCycleEncoder followerEncoder; private final CommandSwerveDrivetrain drive; private final MotionMagicVoltage mmVoltage = new MotionMagicVoltage(0); private final StatusSignal encoderSignal; + private final BaseStatusSignal pinionEncoderSignal; + private final BaseStatusSignal followerEncoderSignal; private static final double kTurretGearRatio = 140/10; // turret ring: 140 teeth, motor pinion: 10 teeth @@ -51,13 +54,17 @@ public TurretIOTalonFX(CommandSwerveDrivetrain drive) { this.turretMotor.getConfigurator().apply(config); this.turretMotor.getConfigurator().apply(mm); - this.pinionEncoder = new DutyCycleEncoder(0); - this.followerEncoder = new DutyCycleEncoder(1); + + CANcoder pinionEncoder = new CANcoder(CanID.TURRET_PINION_CANCODER.getID()); + CANcoder followerEncoder = new CANcoder(CanID.TURRET_FOLLOWER_CANCODER.getID()); + + this.pinionEncoderSignal = pinionEncoder.getAbsolutePosition(); + this.followerEncoderSignal = followerEncoder.getAbsolutePosition(); this.encoderSignal = this.turretMotor.getPosition(); - double pinionEncoderValue = this.pinionEncoder.get(); - double followerEncoderValue = this.followerEncoder.get(); + double pinionEncoderValue = this.pinionEncoderSignal.getValueAsDouble(); + double followerEncoderValue = this.followerEncoderSignal.getValueAsDouble(); // set offset of the turret on startup double turretRotations = TurretMath.getTurretAngleRevs(pinionEncoderValue, followerEncoderValue); @@ -116,8 +123,8 @@ public void setTurretAngle(double fieldAngleDeg) { @Override public void updateInputs(TurretIOInputs inputs) { - inputs.pinionEncoder = this.pinionEncoder.get(); - inputs.followerEncoder = this.followerEncoder.get(); + inputs.pinionEncoder = this.pinionEncoderSignal.getValueAsDouble(); + inputs.followerEncoder = this.followerEncoderSignal.getValueAsDouble(); inputs.turretSetPointDegrees = this.targetAngleDeg; double turretRotations = TurretMath.getTurretAngleRevs(inputs.pinionEncoder, inputs.followerEncoder); @@ -136,6 +143,6 @@ public void updateInputs(TurretIOInputs inputs) { @Override public void refreshData() { - BaseStatusSignal.refreshAll(encoderSignal); + BaseStatusSignal.refreshAll(encoderSignal, pinionEncoderSignal, followerEncoderSignal); } } From 26cd09030ad4a1a8ee966dae384a9867194f103e Mon Sep 17 00:00:00 2001 From: Justin E <63556914+PillageDev@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:35:27 -0500 Subject: [PATCH 17/19] 10 shooter subsystem (#31) * Add shooter subsystem with shot compensation logic and CAN ID updates * Add shooter subsystem initialization in RobotContainer * Refactor shooter and turret subsystems to use new Targeting subsystem and remove old pubsub system * Integrate shooter and turret dependencies into indexer subsystem and enhance ball feeding logic --- .gitignore | 3 +- .idea/copilot.data.migration.edit.xml | 6 - src/main/deploy/pathplanner/navgrid.json | 1 + src/main/java/frc/robot/CanID.java | 10 +- src/main/java/frc/robot/Robot.java | 15 ++- src/main/java/frc/robot/RobotContainer.java | 55 ++++---- .../frc/robot/subsystems/indexer/Indexer.java | 49 ++++++- .../subsystems/indexer/IndexerIOTalonFX.java | 2 +- .../frc/robot/subsystems/shooter/Shooter.java | 39 ++++++ .../robot/subsystems/shooter/ShooterIO.java | 21 +++ .../subsystems/shooter/ShooterIOTalonFX.java | 126 ++++++++++++++++++ .../robot/subsystems/targeting/Targeting.java | 36 +++++ .../frc/robot/subsystems/turret/Turret.java | 65 ++++----- .../subsystems/turret/TurretIOTalonFX.java | 10 +- 14 files changed, 363 insertions(+), 75 deletions(-) delete mode 100644 .idea/copilot.data.migration.edit.xml create mode 100644 src/main/deploy/pathplanner/navgrid.json create mode 100644 src/main/java/frc/robot/subsystems/shooter/Shooter.java create mode 100644 src/main/java/frc/robot/subsystems/shooter/ShooterIO.java create mode 100644 src/main/java/frc/robot/subsystems/shooter/ShooterIOTalonFX.java create mode 100644 src/main/java/frc/robot/subsystems/targeting/Targeting.java diff --git a/.gitignore b/.gitignore index a33898b..8a683b1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ hs_err_pid* replay_pid* .gradle/ build/ + # IntelliJ IDEA -.idea/* +.idea/* \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml deleted file mode 100644 index 8648f94..0000000 --- a/.idea/copilot.data.migration.edit.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/deploy/pathplanner/navgrid.json b/src/main/deploy/pathplanner/navgrid.json new file mode 100644 index 0000000..ac5f521 --- /dev/null +++ b/src/main/deploy/pathplanner/navgrid.json @@ -0,0 +1 @@ +{"field_size":{"x":16.54,"y":8.07},"nodeSizeMeters":0.3,"grid":[[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true],[true,true,true,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,true,true,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,true,true,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,true,true,true,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true]]} \ No newline at end of file diff --git a/src/main/java/frc/robot/CanID.java b/src/main/java/frc/robot/CanID.java index 14b5cf7..7896199 100644 --- a/src/main/java/frc/robot/CanID.java +++ b/src/main/java/frc/robot/CanID.java @@ -4,10 +4,18 @@ * Holder for all CAN device IDs besides drivetrain devices */ public enum CanID { + INDEXER_MOTOR(10), + + FLYWHEEL_MOTOR(20), + HOOD_MOTOR(12), + HOOD_ENCODER(14), + TURRET_MOTOR(13), + INTAKE_MOTOR(9), INTAKE_DEPLOY_MOTOR(11), - INDEXER(10); + + ; private final int deviceID; diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 530ef86..a916faa 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -14,6 +14,7 @@ package frc.robot; import edu.wpi.first.net.PortForwarder; +import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; @@ -35,6 +36,9 @@ * project. */ public class Robot extends LoggedRobot { + private static final double LOOP_PERIOD_S = 0.02; // 20 ms loop period + private static double tickStart = 0; // start time of current tick, updated for each tick, in seconds + private Command autonomousCommand; private RobotContainer robotContainer; @@ -45,7 +49,6 @@ public class Robot extends LoggedRobot { ? 100000000 : // 100 MB 1000000000; // 1 GB - /** * This function is run when the robot is first started up and should be used for any * initialization code. @@ -151,6 +154,7 @@ void SetupLog() { /** This function is called periodically during all modes. */ @Override public void robotPeriodic() { + tickStart = Timer.getFPGATimestamp(); // Runs the Scheduler. This is responsible for polling buttons, adding // newly-scheduled commands, running already-scheduled commands, removing // finished or interrupted commands, and running subsystem periodic() methods. @@ -216,4 +220,13 @@ public void simulationInit() {} /** This function is called periodically whilst in simulation. */ @Override public void simulationPeriodic() {} + + /** + * Utility function to check if a given timestamp is within the current tick. + * @param timestamp timestamp to check, in seconds + * @return true if the timestamp is within the current tick, false otherwise + */ + public static boolean isTimestampInCurrentTick(double timestamp) { + return timestamp >= tickStart && timestamp < tickStart + LOOP_PERIOD_S; + } } \ No newline at end of file diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index d9cfc38..054cf58 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -6,10 +6,7 @@ import static edu.wpi.first.units.Units.*; -import org.littletonrobotics.junction.Logger; - import com.ctre.phoenix6.swerve.SwerveModule.DriveRequestType; -import com.pathplanner.lib.auto.AutoBuilder; import com.ctre.phoenix6.swerve.SwerveRequest; import edu.wpi.first.math.geometry.Rotation2d; @@ -22,11 +19,16 @@ import frc.robot.generated.TunerConstants; import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import frc.robot.subsystems.shooter.Shooter; +import frc.robot.subsystems.targeting.Targeting; import frc.robot.subsystems.turret.Turret; import frc.robot.subsystems.intake.Intake; import frc.robot.subsystems.indexer.Indexer; import frc.robot.subsystems.vision.Vision; +import java.util.HashSet; +import java.util.Set; + public class RobotContainer { private double MaxSpeed = TunerConstants.kSpeedAt12Volts.in(MetersPerSecond); // kSpeedAt12Volts desired top speed private double MaxAngularRate = RotationsPerSecond.of(0.75).in(RadiansPerSecond); // 3/4 of a rotation per second max angular velocity @@ -34,7 +36,7 @@ public class RobotContainer { private final SendableChooser autoChooser; /* Setting up bindings for necessary control of the swerve drive platform */ - private final SwerveRequest.FieldCentric drive = new SwerveRequest.FieldCentric() + private final SwerveRequest.FieldCentric driveCmd = new SwerveRequest.FieldCentric() .withDeadband(MaxSpeed * 0.1).withRotationalDeadband(MaxAngularRate * 0.1) // Add a 10% deadband .withDriveRequestType(DriveRequestType.OpenLoopVoltage); // Use open-loop control for drive motors private final SwerveRequest.SwerveDriveBrake brake = new SwerveRequest.SwerveDriveBrake(); @@ -45,35 +47,40 @@ public class RobotContainer { private final CommandXboxController driverController = new CommandXboxController(0); private final CommandXboxController operatorController = new CommandXboxController(1); - public static CommandSwerveDrivetrain drivetrain; + public static CommandSwerveDrivetrain drive; private final Vision vision; private final Turret turret; private final Intake intake; private final Indexer indexer; + private final Shooter shooter; + private final Targeting targeting; public RobotContainer() { - drivetrain = TunerConstants.createDrivetrain(); - turret = new Turret(drivetrain); - autoChooser = drivetrain.getAutoChooser(); + drive = TunerConstants.createDrivetrain(); + this.turret = new Turret(); + this.vision = new Vision(drive); + this.intake = new Intake(drive); + this.shooter = new Shooter(); + this.targeting = new Targeting(drive, turret); + this.indexer = new Indexer(2, shooter, turret); + + autoChooser = drive.getAutoChooser(); SmartDashboard.putData("Auto Path", autoChooser); - - this.vision = new Vision(drivetrain); - this.intake = new Intake(drivetrain); - this.indexer = new Indexer(2); + configureBindings(); } public static CommandSwerveDrivetrain getDrive() { - return drivetrain; + return drive; } private void configureBindings() { // Note that X is defined as forward according to WPILib convention, // and Y is defined as to the left according to WPILib convention. - drivetrain.setDefaultCommand( + drive.setDefaultCommand( // Drivetrain will execute this command periodically - drivetrain.applyRequest(() -> - drive.withVelocityX(-driverController.getLeftY() * MaxSpeed) // Drive forward with negative Y (forward) + drive.applyRequest(() -> + driveCmd.withVelocityX(-driverController.getLeftY() * MaxSpeed) // Drive forward with negative Y (forward) .withVelocityY(-driverController.getLeftX() * MaxSpeed) // Drive left with negative X (left) .withRotationalRate(-driverController.getRightX() * MaxAngularRate) // Drive counterclockwise with negative X (left) ) @@ -83,23 +90,23 @@ private void configureBindings() { // neutral mode is applied to the drive motors while disabled. final var idle = new SwerveRequest.Idle(); RobotModeTriggers.disabled().whileTrue( - drivetrain.applyRequest(() -> idle).ignoringDisable(true) + drive.applyRequest(() -> idle).ignoringDisable(true) ); - driverController.a().whileTrue(drivetrain.applyRequest(() -> brake)); - driverController.b().whileTrue(drivetrain.applyRequest(() -> + driverController.a().whileTrue(drive.applyRequest(() -> brake)); + driverController.b().whileTrue(drive.applyRequest(() -> point.withModuleDirection(new Rotation2d(-driverController.getLeftY(), -driverController.getLeftX())) )); // Run SysId routines when holding back/start and X/Y. // Note that each routine should be run exactly once in a single log. - driverController.back().and(driverController.y()).whileTrue(drivetrain.sysIdDynamic(Direction.kForward)); - driverController.back().and(driverController.x()).whileTrue(drivetrain.sysIdDynamic(Direction.kReverse)); - driverController.start().and(driverController.y()).whileTrue(drivetrain.sysIdQuasistatic(Direction.kForward)); - driverController.start().and(driverController.x()).whileTrue(drivetrain.sysIdQuasistatic(Direction.kReverse)); + driverController.back().and(driverController.y()).whileTrue(drive.sysIdDynamic(Direction.kForward)); + driverController.back().and(driverController.x()).whileTrue(drive.sysIdDynamic(Direction.kReverse)); + driverController.start().and(driverController.y()).whileTrue(drive.sysIdQuasistatic(Direction.kForward)); + driverController.start().and(driverController.x()).whileTrue(drive.sysIdQuasistatic(Direction.kReverse)); // reset the field-centric heading on left bumper press - driverController.leftBumper().onTrue(drivetrain.runOnce(() -> drivetrain.seedFieldCentric())); + driverController.leftBumper().onTrue(drive.runOnce(() -> drive.seedFieldCentric())); operatorController.a().onTrue(intake.run(() -> intake.deploy())); operatorController.b().onTrue(intake.run(() -> intake.retract())); diff --git a/src/main/java/frc/robot/subsystems/indexer/Indexer.java b/src/main/java/frc/robot/subsystems/indexer/Indexer.java index dbc46fb..9a931ad 100644 --- a/src/main/java/frc/robot/subsystems/indexer/Indexer.java +++ b/src/main/java/frc/robot/subsystems/indexer/Indexer.java @@ -1,31 +1,68 @@ package frc.robot.subsystems.indexer; import frc.lib.subsystem.SpikeSystem; -import frc.robot.subsystems.indexer.IndexerIO; -import frc.robot.subsystems.indexer.IndexerIOTalonFX; import edu.wpi.first.wpilibj.DigitalInput; +import frc.robot.subsystems.shooter.Shooter; +import frc.robot.subsystems.turret.Turret; public class Indexer extends SpikeSystem { private final static double indexerSpeed = 10.0; // Rotations per second + private final Shooter shooter; + private final Turret turret; + private IndexerIO indexerIO; private DigitalInput proximitySensor; - public Indexer(int channel) { + public Indexer(int channel, Shooter shooter, Turret turret) { super("Indexer", new IndexerIO.IndexerIOInputs()); proximitySensor = new DigitalInput(channel); + + this.shooter = shooter; + this.turret = turret; } // Activate motor if proximity sensor detects a ball in the indexer @Override public void onPeriodic() { - if (proximitySensor.get()) { - indexerIO.setSpeed(0.0); - } else { + if (mechanismReadyForBalls()) { + // run the indexer if the mechanisms are ready for balls + // run it regardless of ball in indexer, so that it can feed a ball in if there is one queued up indexerIO.setSpeed(indexerSpeed); + } else if (needsFeeding()) { + // bring the ball to the indexer and stop once we see a ball + indexerIO.setSpeed(indexerSpeed); + } else { + // stop the indexer if the mechanisms aren't ready and we have a ball queued + indexerIO.setSpeed(0.0); } } + /** + * Checks the proximity sensor to see if there is a ball currently queued up in the indexer. + * @return true if there is a ball in the indexer, false otherwise + */ + private boolean hasBallQueued() { + return proximitySensor.get(); + } + + /** + * Determines if the shooter and turret are ready to receive a ball. + * @return true if both the shooter is at target RPS and the turret is at target angle, false otherwise + */ + private boolean mechanismReadyForBalls() { + return shooter.isAtTargetRPS() && turret.isAtTargetAngle(); + } + + /** + * Determines if the indexer needs a ball fed into it. + * True if the mechanisms need a ball and there isn't one already queued up, false otherwise. + * @return true if the indexer needs a ball, false otherwise + */ + public boolean needsFeeding() { + return !hasBallQueued(); + } + @Override protected Runnable setupDataRefresher() { this.indexerIO = new IndexerIOTalonFX(); diff --git a/src/main/java/frc/robot/subsystems/indexer/IndexerIOTalonFX.java b/src/main/java/frc/robot/subsystems/indexer/IndexerIOTalonFX.java index fd3154e..b50ba7d 100644 --- a/src/main/java/frc/robot/subsystems/indexer/IndexerIOTalonFX.java +++ b/src/main/java/frc/robot/subsystems/indexer/IndexerIOTalonFX.java @@ -10,7 +10,7 @@ public class IndexerIOTalonFX implements IORefresher, IndexerIO { private final BaseStatusSignal motorRps; // Rotations per second public IndexerIOTalonFX() { - this.motor = new TalonFX(CanID.INDEXER.getID()); + this.motor = new TalonFX(CanID.INDEXER_MOTOR.getID()); this.motorRps = motor.getRotorVelocity(); } diff --git a/src/main/java/frc/robot/subsystems/shooter/Shooter.java b/src/main/java/frc/robot/subsystems/shooter/Shooter.java new file mode 100644 index 0000000..ac2d74a --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/Shooter.java @@ -0,0 +1,39 @@ +package frc.robot.subsystems.shooter; + +import frc.lib.subsystem.SpikeSystem; +import frc.robot.subsystems.targeting.Targeting; +import frc.robot.subsystems.turret.calc.ShotCompensation; + +public class Shooter extends SpikeSystem { + private static final double SHOOTER_READY_THRESHOLD_RPS = 0.5; // RPS threshold to consider the shooter ready + + private ShooterIO shooterIO; + private double targetRPS = 0.0; + + public Shooter() { + super("Shooter", new ShooterIO.ShooterIOInputs()); + } + + @Override + public void onPeriodic() { + ShotCompensation.AdjustedShot shotData = Targeting.getShotData(); + + if (shotData != null) { + double newTargetRPS = shotData.rpm() / 60.0; + this.targetRPS = newTargetRPS; + + shooterIO.setHoodAngle(shotData.hoodAngleDeg()); + shooterIO.setFlywheelVelocity(newTargetRPS); + } + } + + @Override + protected Runnable setupDataRefresher() { + shooterIO = new ShooterIOTalonFX(); + return useAsyncDataRefresher(shooterIO); + } + + public boolean isAtTargetRPS() { + return Math.abs(super.io.motorRPS - targetRPS) < SHOOTER_READY_THRESHOLD_RPS; + } +} diff --git a/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java new file mode 100644 index 0000000..5285435 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/ShooterIO.java @@ -0,0 +1,21 @@ +package frc.robot.subsystems.shooter; + +import frc.lib.subsystem.BaseIO; +import frc.lib.subsystem.BaseInputClass; +import frc.lib.subsystem.IORefresher; +import org.littletonrobotics.junction.AutoLog; + +public interface ShooterIO extends BaseIO, IORefresher { + @AutoLog + public static class ShooterIOInputs extends BaseInputClass { + public double motorRPS = 0.0; + public double hoodAngle = 15; + public double hoodMotorPosition = 0.0; + public double flywheelSetPointRPS = 0.0; + public double hoodSetPointAngle = 0.0; + } + + void setFlywheelVelocity(double rps); + + void setHoodAngle(double angle); +} diff --git a/src/main/java/frc/robot/subsystems/shooter/ShooterIOTalonFX.java b/src/main/java/frc/robot/subsystems/shooter/ShooterIOTalonFX.java new file mode 100644 index 0000000..85affee --- /dev/null +++ b/src/main/java/frc/robot/subsystems/shooter/ShooterIOTalonFX.java @@ -0,0 +1,126 @@ +package frc.robot.subsystems.shooter; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.Slot0Configs; +import com.ctre.phoenix6.controls.PositionVoltage; +import com.ctre.phoenix6.controls.VelocityVoltage; +import com.ctre.phoenix6.hardware.CANcoder; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.hardware.TalonFXS; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import frc.lib.subsystem.IORefresher; +import frc.robot.CanID; + +public class ShooterIOTalonFX implements IORefresher, ShooterIO { + private static final double HOOD_GEAR_RATIO = 1.0/1.0; // hood pulley teeth / motor pulley teeth (motor revs per hood rev) + + private final TalonFX flywheelMotor; + private final TalonFXS hoodMotor; + private final CANcoder hoodEncoder; // using wcp throughbore; you interface through 'CANcoder' class + + private final double hoodMotorPositionOffset; // offset in motor rotations, calculated at startup, units in motor rotations + + private final VelocityVoltage flywheelVelocityControl = new VelocityVoltage(0); + private final PositionVoltage hoodPositionControl = new PositionVoltage(0); + + private final StatusSignal motorVelocity; // rps + private final StatusSignal hoodMotorPosition; // motor rotations + private final StatusSignal hoodAngle; // degrees + + private double hoodAngleSetPoint = 0.0; + private double flywheelRPSSetPoint = 0.0; + + public ShooterIOTalonFX() { + // hood configs + this.hoodMotor = new TalonFXS(CanID.HOOD_MOTOR.getID()); + + var hoodSlot0 = new Slot0Configs(); + hoodSlot0.kP = 0.1; + hoodSlot0.kI = 0.0; + hoodSlot0.kD = 0.0; + hoodSlot0.kS = 0.0; + hoodSlot0.kV = 0.0; + + this.hoodMotor.getConfigurator().apply(hoodSlot0); + + // flywheel configs + this.flywheelMotor = new TalonFX(CanID.FLYWHEEL_MOTOR.getID()); + + var flywheelSlot0 = new Slot0Configs(); + flywheelSlot0.kP = 0.1; + flywheelSlot0.kI = 0.0; + flywheelSlot0.kD = 0.0; + flywheelSlot0.kS = 0.0; + flywheelSlot0.kV = 0.0; + + this.flywheelMotor.getConfigurator().apply(flywheelSlot0); + + this.hoodEncoder = new CANcoder(CanID.HOOD_ENCODER.getID()); + + CANcoderConfiguration hoodEncoderConfig = new CANcoderConfiguration(); + // constrains the range to [0, 1) + hoodEncoderConfig.MagnetSensor.withAbsoluteSensorDiscontinuityPoint(1.0); + + this.hoodEncoder.getConfigurator().apply(hoodEncoderConfig); + + this.motorVelocity = flywheelMotor.getVelocity(); + this.hoodAngle = hoodEncoder.getAbsolutePosition(); + this.hoodMotorPosition = hoodMotor.getPosition(); + + // force refresh before zero calculations + BaseStatusSignal.refreshAll(motorVelocity, hoodAngle, hoodMotorPosition); + + // convert hood angle to motor rotations and calculate offset + this.hoodMotorPositionOffset = this.hoodMotorPosition.getValueAsDouble() - angleToMotorRotations(this.hoodAngle.getValueAsDouble()); + + BaseStatusSignal.setUpdateFrequencyForAll( + 50.0, + motorVelocity, + hoodAngle, + hoodMotorPosition + ); + + flywheelMotor.optimizeBusUtilization(); + hoodMotor.optimizeBusUtilization(); + } + + @Override + public void refreshData() { + BaseStatusSignal.refreshAll(motorVelocity, hoodAngle, hoodMotorPosition); + } + + @Override + public void updateInputs(ShooterIO.ShooterIOInputs inputs) { + inputs.motorRPS = this.motorVelocity.getValueAsDouble(); + inputs.hoodAngle = this.hoodAngle.getValueAsDouble() * 360; // convert rotations to degrees + inputs.hoodMotorPosition = this.hoodMotorPosition.getValueAsDouble(); + + inputs.flywheelSetPointRPS = this.flywheelRPSSetPoint; + inputs.hoodSetPointAngle = this.hoodAngleSetPoint; + } + + @Override + public void setFlywheelVelocity(double rps) { + this.flywheelRPSSetPoint = rps; + this.flywheelVelocityControl.withVelocity(rps); + + flywheelMotor.setControl(this.flywheelVelocityControl); + } + + @Override + public void setHoodAngle(double angle) { + this.hoodAngleSetPoint = angle; + double motorRotations = angleToMotorRotations(angle); + this.hoodPositionControl.withPosition(hoodMotorPositionOffset + motorRotations); + + this.hoodMotor.setControl(this.hoodPositionControl); + } + + private static double angleToMotorRotations(double angle) { + // rotations = (angle_deg * gear_ratio) / 360 + return angle * HOOD_GEAR_RATIO / 360.0; + } +} diff --git a/src/main/java/frc/robot/subsystems/targeting/Targeting.java b/src/main/java/frc/robot/subsystems/targeting/Targeting.java new file mode 100644 index 0000000..a08e493 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/targeting/Targeting.java @@ -0,0 +1,36 @@ +package frc.robot.subsystems.targeting; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import frc.robot.subsystems.turret.Turret; +import frc.robot.subsystems.turret.calc.ShotCompensation; + +public class Targeting extends SubsystemBase { + private static final double NOMINAL_SHOT_TIME_S = 0.3; // see github issue #23 (https://github.com/Team293/Rebuilt/issues/23) + private static ShotCompensation.AdjustedShot shotData = new ShotCompensation.AdjustedShot(0.0, 0.0, 0.0, 0.0, 0.0); + + private final CommandSwerveDrivetrain drive; + private final Turret turret; + + public Targeting(CommandSwerveDrivetrain drive, Turret turret) { + this.drive = drive; + this.turret = turret; + } + + @Override + public void periodic() { + // calculate the adjusted shot parameters based on the current robot movement and the turret's target position + shotData = ShotCompensation.compensateForMovement( + drive.getPose(), + drive.getState().Speeds, + new Pose2d(turret.getTargetPos(), new Rotation2d()), + NOMINAL_SHOT_TIME_S + ); + } + + public static ShotCompensation.AdjustedShot getShotData() { + return shotData; + } +} diff --git a/src/main/java/frc/robot/subsystems/turret/Turret.java b/src/main/java/frc/robot/subsystems/turret/Turret.java index 1cd05ec..e5c67a8 100644 --- a/src/main/java/frc/robot/subsystems/turret/Turret.java +++ b/src/main/java/frc/robot/subsystems/turret/Turret.java @@ -1,7 +1,5 @@ package frc.robot.subsystems.turret; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Translation2d; import frc.lib.Elastic; import frc.lib.FieldConstants; @@ -9,20 +7,48 @@ import frc.lib.Elastic.NotificationLevel; import frc.lib.subsystem.SpikeSystem; import frc.robot.RobotContainer; -import frc.robot.subsystems.drive.CommandSwerveDrivetrain; +import frc.robot.subsystems.targeting.Targeting; import frc.robot.subsystems.turret.calc.ShotCompensation; import org.littletonrobotics.junction.Logger; public class Turret extends SpikeSystem { - private TurretIOTalonFX turretIO; - private final CommandSwerveDrivetrain drive; + private static final double TURRET_ANGLE_THRESHOLD_DEG = 3.0; // degrees within target angle to be considered "at target" public enum State { TARGETING_HUB, TARGETING_SHUTTLE } + private TurretIOTalonFX turretIO; private State currentState = State.TARGETING_HUB; - private Translation2d targetPos = FieldConstants.Hub.innerCenterPoint.toTranslation2d(); + private double targetAngleDeg = 0.0; + + public Turret() { + super("Turret", new TurretIO.TurretIOInputs()); + Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); + } + + @Override + public void onPeriodic() { + // compensate for robot movement + ShotCompensation.AdjustedShot shotData = Targeting.getShotData(); + + if (shotData != null) { + double newTargetAngleDeg = shotData.turretAngleDeg(); + this.targetAngleDeg = newTargetAngleDeg; + + this.turretIO.setTurretAngle(newTargetAngleDeg); + } + } + + @Override + protected Runnable setupDataRefresher() { + this.turretIO = new TurretIOTalonFX(RobotContainer.getDrive()); + return useAsyncDataRefresher(this.turretIO); + } + + public Translation2d getTargetPos() { + return this.targetPos; + } public void setTargetingHub() { if (currentState != State.TARGETING_HUB) { @@ -46,29 +72,8 @@ public void setTargetingShuttle() { } } - public Turret(CommandSwerveDrivetrain drive) { - super("Turret", new TurretIO.TurretIOInputs()); - Logger.recordOutput("Hub/Center", FieldConstants.Hub.innerCenterPoint); - this.drive = drive; - } - - @Override - public void onPeriodic() { - // compensate for robot movement - ShotCompensation.AdjustedShot targetAngleCompensated = ShotCompensation.compensateForMovement( - drive.getPose(), - drive.getState().Speeds, - new Pose2d(this.targetPos, new Rotation2d()), - 0.3 // see github issue #23 (https://github.com/Team293/Rebuilt/issues/23) - ); - - // set the turret angle to the compensated angle - this.turretIO.setTurretAngle(targetAngleCompensated.turretAngleDeg()); - } - - @Override - protected Runnable setupDataRefresher() { - this.turretIO = new TurretIOTalonFX(RobotContainer.getDrive()); - return useAsyncDataRefresher(this.turretIO); + public boolean isAtTargetAngle() { + double angleError = Math.abs(super.io.turretAngleDegrees - targetAngleDeg); + return angleError < TURRET_ANGLE_THRESHOLD_DEG; } } diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java index cc38b70..43e3c36 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java @@ -74,9 +74,9 @@ public TurretIOTalonFX(CommandSwerveDrivetrain drive) { } @Override - public void setTurretAngle(double fieldAngleDeg) { - this.targetAngleDeg = fieldAngleDeg; - fieldAngleDeg = MathUtil.inputModulus(fieldAngleDeg, 0.0, 360.0); + public void setTurretAngle(double fieldTargetHeadingDeg) { + this.targetAngleDeg = fieldTargetHeadingDeg; + fieldTargetHeadingDeg = MathUtil.inputModulus(fieldTargetHeadingDeg, 0.0, 360.0); // convert robot heading to [0, 360) range double robotHeadingDeg = @@ -90,10 +90,10 @@ public void setTurretAngle(double fieldAngleDeg) { double predictedHeadingDeg = robotHeadingDeg + this.drive.getRobotOmegaDegPerSec() * dt; - // turret angle in (-180, 180] range, where positive is counterclockwise from the field forward direction + // turret angle in (-180, 180] range, where positive is counterclockwise relative to the robot's forward direction, and negative is clockwise double turretAngleDeg = MathUtil.inputModulus( - fieldAngleDeg + predictedHeadingDeg, + fieldTargetHeadingDeg - predictedHeadingDeg, -180.0, 180.0 ); From 133cd4cf5a843603a8503367312e51e85b4aff7f Mon Sep 17 00:00:00 2001 From: NathanEdg Date: Sat, 28 Feb 2026 14:36:40 -0500 Subject: [PATCH 18/19] Add [0, 1) constraints for CANcoder sensor readings --- .../java/frc/robot/subsystems/turret/TurretIOTalonFX.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java index c47e071..0c69132 100644 --- a/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java +++ b/src/main/java/frc/robot/subsystems/turret/TurretIOTalonFX.java @@ -58,6 +58,14 @@ public TurretIOTalonFX(CommandSwerveDrivetrain drive) { CANcoder pinionEncoder = new CANcoder(CanID.TURRET_PINION_CANCODER.getID()); CANcoder followerEncoder = new CANcoder(CanID.TURRET_FOLLOWER_CANCODER.getID()); + CANcoderConfiguration encoderConfig = new CANcoderConfiguration(); + + // constrain sensor readings to [0, 1) + encoderConfig.MagnetSensor.withAbsoluteSensorDiscontinuityPoint(1.0); + + pinionEncoder.getConfigurator().apply(encoderConfig); + followerEncoder.getConfigurator().apply(encoderConfig); + this.pinionEncoderSignal = pinionEncoder.getAbsolutePosition(); this.followerEncoderSignal = followerEncoder.getAbsolutePosition(); From bbffda0b594342e6600838578a271c75ce79efa9 Mon Sep 17 00:00:00 2001 From: NathanEdg Date: Sat, 28 Feb 2026 14:46:53 -0500 Subject: [PATCH 19/19] Fix weird chars in comments --- .../java/frc/robot/generated/TunerConstants.java | 4 ++-- .../subsystems/drive/CommandSwerveDrivetrain.java | 6 +++--- .../frc/robot/subsystems/turret/calc/TurretMath.java | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/frc/robot/generated/TunerConstants.java b/src/main/java/frc/robot/generated/TunerConstants.java index 9aab639..02b995e 100644 --- a/src/main/java/frc/robot/generated/TunerConstants.java +++ b/src/main/java/frc/robot/generated/TunerConstants.java @@ -267,10 +267,10 @@ public TunerSwerveDrivetrain( * unspecified or set to 0 Hz, this is 250 Hz on * CAN FD, and 100 Hz on CAN 2.0. * @param odometryStandardDeviation The standard deviation for odometry calculation - * in the form [x, y, theta]ᵀ, with units in meters + * in the form [x, y, theta], with units in meters * and radians * @param visionStandardDeviation The standard deviation for vision calculation - * in the form [x, y, theta]ᵀ, with units in meters + * in the form [x, y, theta], with units in meters * and radians * @param modules Constants for each specific module */ diff --git a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java index 35641ee..4e774aa 100644 --- a/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java +++ b/src/main/java/frc/robot/subsystems/drive/CommandSwerveDrivetrain.java @@ -189,10 +189,10 @@ public CommandSwerveDrivetrain( * unspecified or set to 0 Hz, this is 250 Hz on * CAN FD, and 100 Hz on CAN 2.0. * @param odometryStandardDeviation The standard deviation for odometry calculation - * in the form [x, y, theta]ᵀ, with units in meters + * in the form [x, y, theta], with units in meters * and radians * @param visionStandardDeviation The standard deviation for vision calculation - * in the form [x, y, theta]ᵀ, with units in meters + * in the form [x, y, theta], with units in meters * and radians * @param modules Constants for each specific module */ @@ -306,7 +306,7 @@ public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampS * @param visionRobotPoseMeters The pose of the robot as measured by the vision camera. * @param timestampSeconds The timestamp of the vision measurement in seconds. * @param visionMeasurementStdDevs Standard deviations of the vision pose measurement - * in the form [x, y, theta]ᵀ, with units in meters and radians. + * in the form [x, y, theta], with units in meters and radians. */ @Override public void addVisionMeasurement( diff --git a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java index dbd66e8..ab3d431 100644 --- a/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java +++ b/src/main/java/frc/robot/subsystems/turret/calc/TurretMath.java @@ -6,10 +6,10 @@ * * Hardware setup (example): * - Turret gear: 140 teeth - * - Encoder A: e.g. 13-tooth gear driving an absolute encoder (reads 0–1 rev) - * - Encoder B: e.g. 11-tooth gear driving an absolute encoder (reads 0–1 rev) + * - Encoder A: e.g. 13-tooth gear driving an absolute encoder (reads 0-1 rev) + * - Encoder B: e.g. 11-tooth gear driving an absolute encoder (reads 0-1 rev) * - * The combination gives unique positions over (encoderA_teeth × encoderB_teeth) teeth. + * The combination gives unique positions over (encoderA_teeth x encoderB_teeth) teeth. * Beyond that range, continuity tracking (unwrapping) is used. */ public class TurretMath { @@ -20,8 +20,8 @@ public class TurretMath { private static final double ENCODER_B_TEETH = 11.0; // Derived encoder combination values - private static final double ENCODER_COMBINED_TEETH = ENCODER_A_TEETH * ENCODER_B_TEETH; // 143 (example) - private static final double ENCODER_COMBINED_PERIOD_REV = ENCODER_COMBINED_TEETH / TURRET_GEAR_TEETH; // ≈1.0214 (example) + private static final double ENCODER_COMBINED_TEETH = ENCODER_A_TEETH * ENCODER_B_TEETH; + private static final double ENCODER_COMBINED_PERIOD_REV = ENCODER_COMBINED_TEETH / TURRET_GEAR_TEETH; private static final double HALF_ENCODER_COMBINED_PERIOD_REV = ENCODER_COMBINED_PERIOD_REV / 2.0; private static final double NORMALIZED_REV = 1.0; // encoder reading range (0..1) @@ -140,7 +140,7 @@ public static double toDegreesWrapped(double turretRevs) { } /** - * Convert turret revolutions to radians, wrapped to [0, 2π). + * Convert turret revolutions to radians, wrapped to [0, 2pi). */ public static double toRadiansWrapped(double turretRevs) { return mod(turretRevs, NORMALIZED_REV) * RAD_PER_REV;