diff --git a/.vscode/settings.json b/.vscode/settings.json index 14a0f19..1259887 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "AHRS", "apriltag", "Arducam", + "Blinken", "Brushless", "cancoder", "CANFD", diff --git a/AdvantageScopeLayout.json b/AdvantageScopeLayout.json index 541881d..d3c3277 100644 --- a/AdvantageScopeLayout.json +++ b/AdvantageScopeLayout.json @@ -3,17 +3,38 @@ { "x": -8, "y": -8, - "width": 1552, - "height": 832, + "width": 2576, + "height": 1408, "state": { "sidebar": { "width": 373, "expanded": [ - "/RealOutputs" + "/RealOutputs", + "/RealOutputs/TagFollowing/Target", + "/RealOutputs/TagFollowing/Targets", + "/RealOutputs/TagFollowing/Follow1", + "/RealOutputs/TagFollowing/Tags", + "/RealOutputs/Box", + "/RealOutputs/ContainmentBox/Vision Demo Box", + "/RealOutputs/SimControlledTarget/1/TagPose/translation", + "/RealOutputs/SimControlledTarget/1", + "/RealOutputs/SimControlledTarget/18", + "/RealOutputs/TagTracking/Tags", + "/RealOutputs/SimControlledTarget/17", + "/RealOutputs/TagTracking/Target", + "/LED", + "/RealOutputs/TagFollowing", + "/RealOutputs/TagFollowing/ChassisSpeeds", + "/Tuning", + "/Tuning/ContainmentBox", + "/Tuning/ContainmentBox/Vision Demo Box", + "/Tuning/VisionResultsStatus", + "/Tuning/VisionDemo", + "/RealOutputs/SwerveStates" ] }, "tabs": { - "selected": 1, + "selected": 2, "tabs": [ { "type": 0, @@ -51,7 +72,7 @@ "type": "swerveStates", "logKey": "/RealOutputs/SwerveStates/MeasuredWheelSpeeds", "logType": "SwerveModuleState[]", - "visible": false, + "visible": true, "options": { "color": "#ff0000", "arrangement": "0,1,2,3" @@ -68,7 +89,7 @@ "type": "mechanism", "logKey": "/RealOutputs/SuperstructureVisualizer/Goal", "logType": "Mechanism2d", - "visible": false, + "visible": true, "options": {} }, { @@ -174,6 +195,187 @@ }, "controlsHeight": 251 }, + { + "type": 3, + "title": "3D Vision Demo", + "controller": { + "sources": [ + { + "type": "robot", + "logKey": "/RealOutputs/Odometry/Robot", + "logType": "Pose2d", + "visible": true, + "options": { + "model": "demo_8032" + } + }, + { + "type": "swerveStates", + "logKey": "/RealOutputs/SwerveStates/MeasuredWheelSpeeds", + "logType": "SwerveModuleState[]", + "visible": true, + "options": { + "color": "#ff0000", + "arrangement": "0,1,2,3" + } + }, + { + "type": "mechanism", + "logKey": "/RealOutputs/SuperstructureVisualizer/Measured", + "logType": "Mechanism2d", + "visible": true, + "options": {} + }, + { + "type": "mechanism", + "logKey": "/RealOutputs/SuperstructureVisualizer/Goal", + "logType": "Mechanism2d", + "visible": false, + "options": {} + }, + { + "type": "mechanism", + "logKey": "/RealOutputs/HangVisualizer/Measured", + "logType": "Mechanism2d", + "visible": true, + "options": {} + }, + { + "type": "gamePiece", + "logKey": "/RealOutputs/ObjectVisualizer/Coral/HeldCoral", + "logType": "Pose3d", + "visible": true, + "options": { + "variant": "None" + } + }, + { + "type": "gamePiece", + "logKey": "/RealOutputs/ObjectVisualizer/Coral/PlacedCoral", + "logType": "Pose3d[]", + "visible": true, + "options": { + "variant": "None" + } + }, + { + "type": "axes", + "logKey": "/RealOutputs/TagFollowing/Follow/TagFilteredPosition", + "logType": "Pose3d", + "visible": true, + "options": {} + }, + { + "type": "ghost", + "logKey": "/RealOutputs/TagFollowing/RobotRawSetpointPose", + "logType": "Pose2d", + "visible": true, + "options": { + "model": "demo_8032", + "color": "#00ffff" + } + }, + { + "type": "mechanism", + "logKey": "/RealOutputs/SuperstructureVisualizer/Goal", + "logType": "Mechanism2d", + "visible": true, + "options": {} + }, + { + "type": "ghost", + "logKey": "/RealOutputs/TagFollowing/RobotSetpointPose", + "logType": "Pose2d", + "visible": true, + "options": { + "model": "demo_8032", + "color": "#00ff00" + } + }, + { + "type": "cone", + "logKey": "/RealOutputs/ObjectVisualizer/Coral/HeldCoral", + "logType": "Pose3d", + "visible": true, + "options": { + "color": "#ffff00", + "position": "center" + } + }, + { + "type": "cone", + "logKey": "/RealOutputs/ContainmentBox/Vision Demo Box/Cones", + "logType": "Pose3d[]", + "visible": true, + "options": { + "color": "#00ff00", + "position": "back" + } + }, + { + "type": "trajectory", + "logKey": "/RealOutputs/ContainmentBox/Vision Demo Box/Outline", + "logType": "Pose2d[]", + "visible": true, + "options": { + "color": "#00ff00", + "size": "normal" + } + }, + { + "type": "aprilTag", + "logKey": "/RealOutputs/TagTracking/Tags/Pose3d", + "logType": "Pose3d[]", + "visible": true, + "options": { + "family": "36h11" + } + }, + { + "type": "cone", + "logKey": "/RealOutputs/TagTracking/Tags/CameraPoses3d", + "logType": "Pose3d[]", + "visible": true, + "options": { + "color": "#00ffff", + "position": "center" + } + }, + { + "type": "axes", + "logKey": "/RealOutputs/SimControlledTarget/18/TagPose", + "logType": "Pose3d", + "visible": true, + "options": {} + }, + { + "type": "axes", + "logKey": "/RealOutputs/SimControlledTarget/17/TagPose", + "logType": "Pose3d", + "visible": true, + "options": {} + } + ], + "game": "Evergreen", + "origin": "blue" + }, + "controllerUUID": "e8hgomb1yslv0cwpvfxkohzvzsz5j7n2", + "renderer": { + "cameraIndex": -1, + "orbitFov": 50, + "cameraPosition": [ + -1.0799999999999996, + 2.573226373964618, + -4.85375699379604 + ], + "cameraTarget": [ + -1.08, + -0.16053570895895541, + -0.41991407324430385 + ] + }, + "controlsHeight": 251 + }, { "type": 2, "title": "Odometry", @@ -253,21 +455,6 @@ "renderer": null, "controlsHeight": 200 }, - { - "type": 2, - "title": "Odometry", - "controller": { - "sources": [], - "game": "2025 Field (Welded)", - "bumpers": "auto", - "origin": "blue", - "orientation": 0, - "size": 30 - }, - "controllerUUID": "zmhi7fxxg1blfr1vdsh7oqjd8pfk5ebs", - "renderer": null, - "controlsHeight": 200 - }, { "type": 10, "title": "Superstructure", @@ -321,6 +508,56 @@ "renderer": null, "controlsHeight": 200 }, + { + "type": 9, + "title": "Tag Following Speeds", + "controller": { + "sources": [ + { + "type": "rotation", + "logKey": "/RealOutputs/Odometry/Robot/rotation", + "logType": "Rotation2d", + "visible": true, + "options": {} + }, + { + "type": "chassisSpeeds", + "logKey": "/RealOutputs/TagFollowing/ChassisSpeeds/FilteredSpeeds", + "logType": "ChassisSpeeds", + "visible": true, + "options": { + "color": "#ff00ff" + } + }, + { + "type": "chassisSpeeds", + "logKey": "/RealOutputs/TagFollowing/ChassisSpeeds/Speeds", + "logType": "ChassisSpeeds", + "visible": true, + "options": { + "color": "#ffff00" + } + }, + { + "type": "states", + "logKey": "/RealOutputs/SwerveStates/DesiredWheelSpeeds", + "logType": "SwerveModuleState[]", + "visible": true, + "options": { + "color": "#ff0000", + "arrangement": "0,1,2,3" + } + } + ], + "maxSpeed": 5, + "sizeX": 0.65, + "sizeY": 0.65, + "orientation": 1 + }, + "controllerUUID": "so82myjp23e7sa47u18vp0szp9c1zoki", + "renderer": null, + "controlsHeight": 200 + }, { "type": 9, "title": "Swerve Processed", @@ -466,125 +703,6 @@ }, "controlsHeight": 0 }, - { - "type": 1, - "title": "Elevator Position", - "controller": { - "leftSources": [ - { - "type": "stepped", - "logKey": "/RealOutputs/Elevator/measuredHeightMeters", - "logType": "Number", - "visible": true, - "options": { - "color": "#af2437", - "size": "normal" - } - }, - { - "type": "stepped", - "logKey": "/RealOutputs/Elevator/goalHeightMeters", - "logType": "Number", - "visible": true, - "options": { - "color": "#2b66a2", - "size": "normal" - } - } - ], - "rightSources": [], - "discreteSources": [ - { - "type": "stripes", - "logKey": "/RealOutputs/Elevator/shouldRunProfiled", - "logType": "Boolean", - "visible": true, - "options": { - "color": "#2b66a2" - } - }, - { - "type": "stripes", - "logKey": "/RealOutputs/Elevator/atGoal", - "logType": "Boolean", - "visible": true, - "options": { - "color": "#e5b31b" - } - } - ], - "leftLockedRange": null, - "rightLockedRange": null, - "leftUnitConversion": { - "type": null, - "factor": 1 - }, - "rightUnitConversion": { - "type": null, - "factor": 1 - }, - "leftFilter": 0, - "rightFilter": 0 - }, - "controllerUUID": "oiu5oi42qt8vfmq0k0h1w7bsfl7fkcnn", - "renderer": null, - "controlsHeight": 200 - }, - { - "type": 1, - "title": "Wrist Position", - "controller": { - "leftSources": [ - { - "type": "stepped", - "logKey": "/RealOutputs/Wrist/goalDegrees", - "logType": "Number", - "visible": true, - "options": { - "color": "#2b66a2", - "size": "normal" - } - }, - { - "type": "stepped", - "logKey": "/RealOutputs/Wrist/measuredDegrees", - "logType": "Number", - "visible": true, - "options": { - "color": "#e5b31b", - "size": "normal" - } - } - ], - "rightSources": [], - "discreteSources": [ - { - "type": "stripes", - "logKey": "/DriverStation/Enabled", - "logType": "Boolean", - "visible": true, - "options": { - "color": "#af2437" - } - } - ], - "leftLockedRange": null, - "rightLockedRange": null, - "leftUnitConversion": { - "type": null, - "factor": 1 - }, - "rightUnitConversion": { - "type": null, - "factor": 1 - }, - "leftFilter": 0, - "rightFilter": 0 - }, - "controllerUUID": "2zzgpw1p2fbc52n945hh8ii98enf3t2t", - "renderer": null, - "controlsHeight": 200 - }, { "type": 12, "title": "Metadata", @@ -598,6 +716,29 @@ } } ], - "satellites": [], - "version": "4.1.0" + "satellites": [ + { + "x": 1788, + "y": 642, + "width": 498, + "height": 498, + "state": { + "type": 9, + "visualizer": null + }, + "uuid": "rimbjrz22qw8iu6qzsjs9l9ebw1jgshb" + }, + { + "x": 1758, + "y": 106, + "width": 545, + "height": 545, + "state": { + "type": 9, + "visualizer": null + }, + "uuid": "so82myjp23e7sa47u18vp0szp9c1zoki" + } + ], + "version": "4.1.5" } diff --git a/README.md b/README.md index adad345..914fed2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This project contains robot code for 8032's robot for FRC 2025 game Reefscape. -![AdvantageScope_WPILib_76WDTWyWqM-ezgif com-optimize](https://github.com/user-attachments/assets/29608a4a-011a-4e52-b6e9-f55b69552322) +![Simulation Demo Video](https://github.com/user-attachments/assets/82d81fd8-b114-4d88-bd92-67431d91ece3) ## Software Features @@ -23,6 +23,10 @@ This project contains robot code for 8032's robot for FRC 2025 game Reefscape. * One button auto align for driver, automatically finds nearest scoring location +Driver Xbox Control Scheme +Operator Xbox Control Scheme + + #### Superstructure * State based control to automatically reach setpoints while avoiding collisions * Detection for when Coral is held, triggering different states @@ -31,8 +35,8 @@ This project contains robot code for 8032's robot for FRC 2025 game Reefscape. #### Vision -* PhotonVision based full field localization, robot always new where it was -* Smart camera placement to view relevant April Tags +* PhotonVision based full field localization +* Smart camera placement to view April Tags * Custom advanced filtering and standard deviations calculations to throw out bad results and trust good results more * Kalman filter to integrate vision results with odometry data * Allowed for smart auto alignment system and consistent autos when localization was used in pathing @@ -46,7 +50,10 @@ This project contains robot code for 8032's robot for FRC 2025 game Reefscape.
Diagram of code structure +Subsystem design with AKit IO layers. ![IO Layer Diagram](https://github.com/user-attachments/assets/2ff990a1-d31f-4188-9fbc-afdf1ed5c5f0) + +Overall command based design. ![Command Based Diagram](https://github.com/user-attachments/assets/36cf5fe0-066d-4685-ba1a-818f49612820)
diff --git a/build.gradle b/build.gradle index 19a74b9..09c5c06 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2025.3.1" + id "edu.wpi.first.GradleRIO" version "2025.3.2" id "com.peterabeles.gversion" version "1.10" id "com.diffplug.spotless" version "6.12.0" } diff --git a/elastic-layout-demo.json b/elastic-layout-demo.json index 61105c9..95eee9f 100644 --- a/elastic-layout-demo.json +++ b/elastic-layout-demo.json @@ -230,12 +230,12 @@ "topic": "/SmartDashboard/Heading Degrees", "period": 0.06, "data_type": "double", - "start_angle": 0.0, - "end_angle": 360.0, - "min_value": 0.0, - "max_value": 360.0, + "start_angle": -180.0, + "end_angle": 180.0, + "min_value": -180.0, + "max_value": 180.0, "number_of_labels": 8, - "wrap_value": false, + "wrap_value": true, "show_pointer": true, "show_ticks": true } @@ -264,14 +264,42 @@ { "name": "Autonomous", "grid_layout": { - "layouts": [], + "layouts": [ + { + "title": "photonvision", + "x": 1152.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "List Layout", + "properties": { + "label_position": "TOP" + }, + "children": [ + { + "title": "ledModeRequest", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Text Display", + "properties": { + "topic": "/photonvision/ledModeRequest", + "period": 0.06, + "data_type": "int", + "show_submit_button": false + } + } + ] + } + ], "containers": [ { "title": "Alerts", "x": 1024.0, - "y": 0.0, + "y": 128.0, "width": 256.0, - "height": 512.0, + "height": 384.0, "type": "Alerts", "properties": { "topic": "/SmartDashboard/Alerts", @@ -438,6 +466,521 @@ "period": 0.06, "sort_options": false } + }, + { + "title": "Has Vision", + "x": 1024.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/SmartDashboard/Has Vision", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + } + ] + } + }, + { + "name": "VisionDemo", + "grid_layout": { + "layouts": [ + { + "title": "TagTracking", + "x": 896.0, + "y": 128.0, + "width": 128.0, + "height": 128.0, + "type": "List Layout", + "properties": { + "label_position": "TOP" + }, + "children": [ + { + "title": "TotalDetected", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Text Display", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagTracking/TotalDetected", + "period": 0.06, + "data_type": "int", + "show_submit_button": false + } + }, + { + "title": "TotalFiltered", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Text Display", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagTracking/TotalFiltered", + "period": 0.06, + "data_type": "int", + "show_submit_button": false + } + }, + { + "title": "noTargets", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagTracking/noTargets", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "noTargetsStable", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagTracking/noTargetsStable", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + } + ] + }, + { + "title": "ChassisSpeeds", + "x": 640.0, + "y": 0.0, + "width": 128.0, + "height": 256.0, + "type": "List Layout", + "properties": { + "label_position": "TOP" + }, + "children": [ + { + "title": "IsAntiJitter", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/ChassisSpeeds/IsAntiJitter", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "IsSafeToAntiJitter", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/ChassisSpeeds/IsSafeToAntiJitter", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "hasStableTarget", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/ChassisSpeeds/hasStableTarget", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + } + ] + }, + { + "title": "TagFollowing", + "x": 512.0, + "y": 0.0, + "width": 128.0, + "height": 256.0, + "type": "List Layout", + "properties": { + "label_position": "TOP" + }, + "children": [ + { + "title": "AtReference", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/AtReference", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "AtReferenceStable", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/AtReferenceStable", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "DeltaTime", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Text Display", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/DeltaTime", + "period": 0.06, + "data_type": "double", + "show_submit_button": false + } + }, + { + "title": "LEDPattern", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Text Display", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/LEDPattern", + "period": 0.06, + "data_type": "string", + "show_submit_button": false + } + } + ] + } + ], + "containers": [ + { + "title": "Alerts", + "x": 1024.0, + "y": 128.0, + "width": 256.0, + "height": 256.0, + "type": "Alerts", + "properties": { + "topic": "/SmartDashboard/Alerts", + "period": 0.06 + } + }, + { + "title": "Heading Degrees", + "x": 256.0, + "y": 0.0, + "width": 256.0, + "height": 256.0, + "type": "Radial Gauge", + "properties": { + "topic": "/SmartDashboard/Heading Degrees", + "period": 0.06, + "data_type": "double", + "start_angle": -180.0, + "end_angle": 180.0, + "min_value": -180.0, + "max_value": 180.0, + "number_of_labels": 8, + "wrap_value": true, + "show_pointer": true, + "show_ticks": true + } + }, + { + "title": "Target Heading", + "x": 256.0, + "y": 256.0, + "width": 256.0, + "height": 256.0, + "type": "Radial Gauge", + "properties": { + "topic": "/SmartDashboard/Target Heading", + "period": 0.033, + "data_type": "double", + "start_angle": -180.0, + "end_angle": 180.0, + "min_value": -180.0, + "max_value": 180.0, + "number_of_labels": 8, + "wrap_value": true, + "show_pointer": true, + "show_ticks": true + } + }, + { + "title": "CommsDisableCount", + "x": 1152.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Text Display", + "properties": { + "topic": "/AdvantageKit/SystemStats/CommsDisableCount", + "period": 0.06, + "data_type": "int", + "show_submit_button": false + } + }, + { + "title": "BrownedOut", + "x": 1024.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/AdvantageKit/SystemStats/BrownedOut", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "Superstructure Aim", + "x": 0.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Toggle Switch", + "properties": { + "topic": "/SmartDashboard/Superstructure Aim", + "period": 0.06, + "data_type": "boolean" + } + }, + { + "title": "Follow Tag", + "x": 0.0, + "y": 128.0, + "width": 256.0, + "height": 128.0, + "type": "Command", + "properties": { + "topic": "/SmartDashboard/Follow Tag", + "period": 0.06, + "show_type": false + } + }, + { + "title": "Aim At Tag", + "x": 0.0, + "y": 256.0, + "width": 256.0, + "height": 128.0, + "type": "Command", + "properties": { + "topic": "/SmartDashboard/Aim At Tag", + "period": 0.06, + "show_type": false + } + }, + { + "title": "FMSInfo", + "x": 512.0, + "y": 384.0, + "width": 384.0, + "height": 128.0, + "type": "FMSInfo", + "properties": { + "topic": "/FMSInfo", + "period": 0.06 + } + }, + { + "title": "Drive", + "x": 0.0, + "y": 384.0, + "width": 256.0, + "height": 128.0, + "type": "Subsystem", + "properties": { + "topic": "/SmartDashboard/Drive", + "period": 0.06 + } + }, + { + "title": "SaftyMode", + "x": 768.0, + "y": 0.0, + "width": 256.0, + "height": 128.0, + "type": "Large Text Display", + "properties": { + "topic": "/AdvantageKit/RealOutputs/TagFollowing/Result/SaftyMode", + "period": 0.06, + "data_type": "string" + } + }, + { + "title": "At Goal?", + "x": 640.0, + "y": 256.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/SmartDashboard/At Goal?", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "Has Coral?", + "x": 512.0, + "y": 256.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/SmartDashboard/Has Coral?", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "Reset To Center", + "x": 768.0, + "y": 256.0, + "width": 256.0, + "height": 128.0, + "type": "Command", + "properties": { + "topic": "/SmartDashboard/Reset To Center", + "period": 0.06, + "show_type": false + } + }, + { + "title": "BatteryVoltage", + "x": 1024.0, + "y": 384.0, + "width": 256.0, + "height": 128.0, + "type": "Graph", + "properties": { + "topic": "/AdvantageKit/SystemStats/BatteryVoltage", + "period": 0.033, + "data_type": "double", + "time_displayed": 5.0, + "color": 4278238420, + "line_width": 2.0 + } + }, + { + "title": "Has Vision", + "x": 896.0, + "y": 384.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/SmartDashboard/Has Vision", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "Is Tracking Tag?", + "x": 128.0, + "y": 0.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/SmartDashboard/Is Tracking Tag?", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } + }, + { + "title": "Is In Box?", + "x": 768.0, + "y": 128.0, + "width": 128.0, + "height": 128.0, + "type": "Boolean Box", + "properties": { + "topic": "/SmartDashboard/Is In Box?", + "period": 0.06, + "data_type": "boolean", + "true_color": 4283215696, + "false_color": 4294198070, + "true_icon": "None", + "false_icon": "None" + } } ] } @@ -747,6 +1290,19 @@ "period": 0.06, "show_type": false } + }, + { + "title": "Reset To Center", + "x": 0.0, + "y": 0.0, + "width": 256.0, + "height": 128.0, + "type": "Command", + "properties": { + "topic": "/SmartDashboard/Reset To Center", + "period": 0.06, + "show_type": false + } } ] } diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index 326f3c2..8702ba7 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -2,6 +2,7 @@ import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.RobotController; @@ -21,9 +22,13 @@ public final class Constants { public static final RobotType PRIMARY_ROBOT_TYPE = RobotType.COMP_BOT_2025; private static RobotType robotType; - public static final boolean TUNING_MODE = true; + public static final boolean TUNING_MODE = false; - public static final boolean HIDE_COMMAND_LOOP_OVERRUN = true; + private static final boolean IS_ON_FIELD = false; + private static final boolean VISION_DEMO_MODE = true; + + public static final double DEMO_SPEED_TRANSLATION_SPEED_COFICIENT = 1; + public static final int VISION_DEMO_TAG_ID = 18; /** Enables all test plan autos in the auto chooser. */ public static final boolean RUNNING_TEST_PLANS = false; @@ -57,6 +62,17 @@ public static Mode getMode() { }; } + public static boolean isOnField() { + if (DriverStation.isFMSAttached()) { + return true; + } + return IS_ON_FIELD; + } + + public static boolean isDemoMode() { + return !isOnField() && VISION_DEMO_MODE; + } + public enum Mode { /** Running on a real robot. */ REAL, diff --git a/src/main/java/frc/robot/FieldConstants.java b/src/main/java/frc/robot/FieldConstants.java index beaf093..d0504c1 100644 --- a/src/main/java/frc/robot/FieldConstants.java +++ b/src/main/java/frc/robot/FieldConstants.java @@ -8,6 +8,8 @@ package frc.robot; import com.pathplanner.lib.util.FlippingUtil; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.apriltag.AprilTagFields; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Rotation2d; @@ -34,8 +36,12 @@ * @see https://docs.wpilib.org/en/stable/docs/software/basic-programming/coordinate-system.html */ public class FieldConstants { - public static final double fieldLength = Units.inchesToMeters(690.876); - public static final double fieldWidth = Units.inchesToMeters(317); + + public static final AprilTagFieldLayout FIELD_TAGS = + AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeWelded); + + public static final Translation2d FIELD = + new Translation2d(FIELD_TAGS.getFieldLength(), FIELD_TAGS.getFieldWidth()); public static final double startingLineX = Units.inchesToMeters(299.438); // Measured from the inside of starting line diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 5b6b36c..272cd57 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -6,19 +6,23 @@ import com.pathplanner.lib.auto.NamedCommands; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; import edu.wpi.first.math.geometry.Transform2d; import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; import edu.wpi.first.math.util.Units; import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.GenericHID.RumbleType; import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj.XboxController; import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.button.CommandXboxController; import edu.wpi.first.wpilibj2.command.button.RobotModeTriggers; @@ -29,6 +33,11 @@ import frc.robot.commands.ManualAlignCommands; import frc.robot.commands.controllers.JoystickInputController; import frc.robot.commands.controllers.SpeedLevelController; +import frc.robot.commands.visionDemo.AimAtTagMode; +import frc.robot.commands.visionDemo.ContainmentBox; +import frc.robot.commands.visionDemo.FollowTagMode; +import frc.robot.commands.visionDemo.TagFollowingVision; +import frc.robot.commands.visionDemo.VisionDemoCommand; import frc.robot.subsystems.dashboard.DriverDashboard; import frc.robot.subsystems.drive.Drive; import frc.robot.subsystems.drive.DriveConstants; @@ -43,7 +52,10 @@ import frc.robot.subsystems.hang.HangIO; import frc.robot.subsystems.hang.HangIOHardware; import frc.robot.subsystems.hang.HangIOSim; +import frc.robot.subsystems.led.BlinkenLEDPattern; import frc.robot.subsystems.led.LEDConstants; +import frc.robot.subsystems.led.LEDStripIOBlinken; +import frc.robot.subsystems.led.LEDStripIOSim; import frc.robot.subsystems.led.LEDSubsystem; import frc.robot.subsystems.superstructure.Superstructure; import frc.robot.subsystems.superstructure.Superstructure.State; @@ -69,6 +81,7 @@ import frc.robot.subsystems.vision.AprilTagVision; import frc.robot.subsystems.vision.CameraIOPhotonVision; import frc.robot.subsystems.vision.CameraIOSim; +import frc.robot.subsystems.vision.SimControlledTarget; import frc.robot.subsystems.vision.VisionConstants; import frc.robot.subsystems.visualizer.ObjectVisualizer; import frc.robot.utility.Elastic; @@ -78,8 +91,8 @@ import java.util.Arrays; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.BooleanSupplier; import java.util.function.DoubleSupplier; -import java.util.function.Function; import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; /** @@ -104,7 +117,7 @@ public class RobotContainer { private final Hang hang; - private final LEDSubsystem ledSubsystem = new LEDSubsystem(LEDConstants.PWM_PORTS); + private final LEDSubsystem ledSubsystem; // Controller private final CommandXboxController driverController = new CommandXboxController(0); @@ -166,6 +179,12 @@ public RobotContainer() { coralWrist = new Wrist(new WristIOHardware(WristConstants.WRIST_CONFIG)); sensor = new Sensor(new SensorIOBeam()); coralIntake = new Intake(new IntakeIOHardware(IntakeConstants.CORAL_INTAKE_CONFIG), sensor); + ledSubsystem = + new LEDSubsystem( + new LEDStripIOBlinken( + LEDConstants.LEDS_STRIP_2025_LEFT, LEDConstants.DEFAULT_PATTERN), + new LEDStripIOBlinken( + LEDConstants.LEDS_STRIP_2025_RIGHT, LEDConstants.DEFAULT_PATTERN)); break; case WOOD_BOT_TWO_2025: @@ -179,12 +198,13 @@ public RobotContainer() { new ModuleIOSparkMax(ModuleConstants.FRONT_RIGHT_MODULE_CONFIG), new ModuleIOSparkMax(ModuleConstants.BACK_LEFT_MODULE_CONFIG), new ModuleIOSparkMax(ModuleConstants.BACK_RIGHT_MODULE_CONFIG)); - vision = new AprilTagVision(new CameraIOPhotonVision(VisionConstants.WOODV2_LEFT_CAMERA)); + vision = new AprilTagVision(new CameraIOPhotonVision(VisionConstants.WOOD_V2_LEFT_CAMERA)); elevator = new Elevator(new ElevatorIO() {}); hang = new Hang(new HangIO() {}); coralWrist = new Wrist(new WristIO() {}); sensor = new Sensor(new SensorIO() {}); coralIntake = new Intake(new IntakeIO() {}, sensor); + ledSubsystem = new LEDSubsystem(); break; case SIM_BOT: @@ -208,6 +228,7 @@ public RobotContainer() { coralWrist = new Wrist(new WristIOSim(WristConstants.WRIST_CONFIG)); sensor = new Sensor(new SensorIOSim()); coralIntake = new Intake(new IntakeIOSim(), sensor); + ledSubsystem = new LEDSubsystem(new LEDStripIOSim(LEDConstants.DEFAULT_PATTERN)); break; default: @@ -225,6 +246,7 @@ public RobotContainer() { coralWrist = new Wrist(new WristIO() {}); sensor = new Sensor(new SensorIO() {}); coralIntake = new Intake(new IntakeIO() {}, sensor); + ledSubsystem = new LEDSubsystem(); break; } @@ -232,16 +254,37 @@ public RobotContainer() { superstructure = new Superstructure(elevator, coralWrist, coralIntake); // Vision setup - // vision.setLastRobotPoseSupplier(drive::getRobotPose); - vision.addVisionEstimateConsumer( - (estimate) -> { - if (estimate.status().isSuccess() && Constants.getMode() != Mode.SIM) { - drive.addVisionMeasurement( - estimate.robotPose().toPose2d(), - estimate.timestampSeconds(), - estimate.standardDeviations()); - } - }); + vision.setFieldTags( + Constants.isOnField() ? FieldConstants.FIELD_TAGS : VisionConstants.BLANK_FIELD); + + vision.filterBasedOnLastPose(false, drive::getRobotPose); + + if (!Constants.isOnField() && Constants.isDemoMode()) { + Elastic.selectTab("Vision Demo"); + + Pose3d startPose = + new Pose3d( + new Translation3d( + FieldConstants.FIELD.getX() / 2, FieldConstants.FIELD.getY() / 2, 1), + Rotation3d.kZero); + + XboxController controller = new XboxController(3); + TagFollowingVision.debugModeSupplier = controller::getYButton; + vision.addSimulatedTarget( + new SimControlledTarget(Constants.VISION_DEMO_TAG_ID, startPose, controller)); + drive.resetPose(startPose.toPose2d()); + + } else { + vision.addVisionEstimateConsumer( + (estimate) -> { + if (estimate.status().isSuccess() && Constants.getMode() != Mode.SIM) { + drive.addVisionMeasurement( + estimate.robotPose().toPose2d(), + estimate.timestampSeconds(), + estimate.standardDeviations()); + } + }); + } coralSimulator = new ObjectVisualizer("Coral", drive::getRobotPose, superstructure::getEndPose); @@ -250,7 +293,6 @@ public RobotContainer() { // Can also use AutoBuilder.buildAutoChooser(); instead of SendableChooser to auto populate registerNamedCommands(); autoChooser = new LoggedDashboardChooser<>("Auto Chooser", new SendableChooser()); - // autoChooser = new LoggedDashboardChooser<>("Auto Chooser", AutoBuilder.buildAutoChooser()); autoChooser.addDefaultOption("None", Commands.none()); // Configure autos @@ -262,12 +304,16 @@ public RobotContainer() { notPrimaryBotAlert.set(Constants.getRobot() != Constants.PRIMARY_ROBOT_TYPE); // Hide controller missing warnings for sim - DriverStation.silenceJoystickConnectionWarning(Constants.getMode() != Mode.REAL); + DriverStation.silenceJoystickConnectionWarning( + Constants.getMode() != Mode.REAL || Constants.isDemoMode()); initDashboard(); - // Configure the button bindings - configureControllerBindings(); + configureAlertTriggers(); + + configureDriverControllerBindings( + driverController, Constants.isOnField(), Constants.isOnField()); + configureOperatorControllerBindings(operatorController); } /** Configure drive dashboard object */ @@ -282,20 +328,25 @@ private void initDashboard() { dashboard.setAutoAlignPoseSupplier(AdaptiveAutoAlignCommands::getCurrentAutoAlignGoal); - dashboard.setHasVisionEstimateSupplier(vision::hasVisionEstimate, 0.1); + dashboard.setHasVisionEstimateSupplier(vision::hasStableVisionEstimate); dashboard.setSensorSuppliers( coralIntake::usingSensor, () -> coralIntake.hasCoral().orElse(false)); dashboard.setHangSuppliers(hang::getMeasuredDegrees); dashboard.setSuperstructureAtGoal(superstructure::atGoal); - dashboard.addCommand("Reset Pose", () -> drive.resetPose(new Pose2d()), true); + dashboard.addCommand("Reset Pose", () -> drive.resetPose(Pose2d.kZero), true); + dashboard.addCommand( "Reset Rotation", - drive.runOnce( - () -> - drive.resetPose( - new Pose2d(drive.getRobotPose().getTranslation(), Rotation2d.kZero))), + () -> drive.resetPose(new Pose2d(drive.getRobotPose().getTranslation(), Rotation2d.kZero)), + true); + + dashboard.addCommand( + "Reset To Center", + () -> + drive.resetPose( + new Pose2d(FieldConstants.FIELD.div(2), drive.getRobotPose().getRotation())), true); dashboard.addCommand( @@ -306,6 +357,51 @@ private void initDashboard() { new Transform2d( DRIVE_CONFIG.bumperCornerToCorner().getX() / 2, 0, Rotation2d.kPi))), true); + + if (Constants.isDemoMode()) { + SmartDashboard.putBoolean("Superstructure Aim", true); + BooleanSupplier useSuperstructure = + () -> SmartDashboard.getBoolean("Superstructure Aim", true); + + double length = Units.feetToMeters(10); + double width = Units.feetToMeters(10); + + Translation2d boxCenter = FieldConstants.FIELD.div(2); + + ContainmentBox box = + new ContainmentBox( + "Vision Demo Box", boxCenter, length, width, DRIVE_CONFIG.bumperCornerToCorner()); + + dashboard.setIsInBox(() -> box.contains(drive.getRobotPose())); + + TagFollowingVision tagFollowingVision = + new TagFollowingVision(vision, Constants.VISION_DEMO_TAG_ID); + + SmartDashboard.putData( + "Aim At Tag", + new VisionDemoCommand( + drive, + tagFollowingVision, + elevator, + coralWrist, + ledSubsystem, + new AimAtTagMode(useSuperstructure), + box) + .onlyIf(() -> box.contains(drive.getRobotPose())) + .withName("Aim At Tag")); + SmartDashboard.putData( + "Follow Tag", + new VisionDemoCommand( + drive, + tagFollowingVision, + elevator, + coralWrist, + ledSubsystem, + new FollowTagMode(new Translation2d(2, 0), useSuperstructure), + box) + .onlyIf(() -> box.contains(drive.getRobotPose())) + .withName("Follow Tag")); + } } public void updateAlerts() { @@ -318,16 +414,8 @@ public void updateAlerts() { || !DriverStation.getJoystickIsXbox(operatorController.getHID().getPort())); } - /** Define button->command mappings. */ - private void configureControllerBindings() { - CommandScheduler.getInstance().getActiveButtonLoop().clear(); - configureDriverControllerBindings(driverController, true); - configureOperatorControllerBindings(operatorController, false); - configureAlertTriggers(); - } - private void configureDriverControllerBindings( - CommandXboxController xbox, boolean includeAutoAlign) { + CommandXboxController xbox, boolean includeAutoAlign, boolean includeAngleAlign) { final Trigger useFieldRelative = new Trigger(new OverrideSwitch(xbox.y(), OverrideSwitch.Mode.TOGGLE, true)); @@ -383,6 +471,10 @@ private void configureDriverControllerBindings( .or(RobotModeTriggers.disabled()) .onTrue(drive.runOnce(drive::stop).withName("CANCEL and stop")); + xbox.b() + .debounce(0.5) + .whileTrue(drive.run(drive::stopUsingForwardArrangement).withName("ORIENT and stop")); + // Reset the gyro heading xbox.start() .debounce(0.3) @@ -398,17 +490,29 @@ private void configureDriverControllerBindings( xbox.back().onTrue(superstructure.runAction(Superstructure.State.STOW_LOW)); - final BiConsumer configureAlignmentAuto = - (trigger, command) -> { - trigger.onTrue(command.until(() -> input.getOmegaRadiansPerSecond() != 0)); - }; - - configureAlignmentAuto.accept( - xbox.povRight(), ManualAlignCommands.alignToSourceRight(drive, input)); - configureAlignmentAuto.accept( - xbox.povLeft(), ManualAlignCommands.alignToSourceLeft(drive, input)); - configureAlignmentAuto.accept(xbox.povDown(), ManualAlignCommands.alignToCageAdv(drive, input)); - configureAlignmentAuto.accept(xbox.povUp(), ManualAlignCommands.alignToReef(drive, input)); + if (includeAngleAlign) { + final BiConsumer configureAlignmentAuto = + (trigger, command) -> { + trigger.onTrue(command.until(() -> input.getOmegaRadiansPerSecond() != 0)); + }; + + configureAlignmentAuto.accept( + xbox.povRight(), ManualAlignCommands.alignToSourceRight(drive, input)); + configureAlignmentAuto.accept( + xbox.povLeft(), ManualAlignCommands.alignToSourceLeft(drive, input)); + configureAlignmentAuto.accept( + xbox.povDown(), ManualAlignCommands.alignToCageAdv(drive, input)); + configureAlignmentAuto.accept(xbox.povUp(), ManualAlignCommands.alignToReef(drive, input)); + } else { + for (int pov = 0; pov < 360; pov += 45) { + final Translation2d translation = new Translation2d(1, Rotation2d.fromDegrees(-pov)); + final ChassisSpeeds speeds = + new ChassisSpeeds( + translation.getX(), translation.getY(), input.getOmegaRadiansPerSecond()); + xbox.pov(pov) + .whileTrue(drive.run(() -> drive.setRobotSpeeds(speeds)).withName("Drive POV " + pov)); + } + } if (includeAutoAlign) { // Align to reef @@ -469,8 +573,7 @@ private void configureDriverControllerBindings( } } - private void configureOperatorControllerBindings( - CommandXboxController xbox, boolean hangSetpoints) { + private void configureOperatorControllerBindings(CommandXboxController xbox) { // Enable @@ -507,13 +610,12 @@ private void configureOperatorControllerBindings( .and(xbox.rightTrigger()) .and(superstructure::atGoal) .whileTrue(superstructure.runWheels(level)) - .onTrue( - Commands.runOnce( - () -> - coralSimulator.placeHeldItemOnNearestWithInterpolation( - FieldConstants.Reef.coralPlacementPositions, - Units.inchesToMeters(15), - 0.2))); + .onTrue(Commands.print("Spinning wheels to place coral")) + .whileTrue( + coralSimulator.placeHeldItemOnNearestWithInterpolation( + FieldConstants.Reef.coralPlacementPositions, + Units.inchesToMeters(15), + 0.2)); } else if (level.isIntake()) { trigger.whileTrue( superstructure.runWheels(level).until(() -> coralIntake.hasCoral().orElse(false))); @@ -538,8 +640,6 @@ private void configureOperatorControllerBindings( configureOperatorControllerBindingLevel.accept(xbox.leftTrigger(), Superstructure.State.INTAKE); - final Trigger anyButton = xbox.a().or(xbox.x()).or(xbox.y()).or(xbox.b()); - final Trigger algae = new Trigger(() -> false); configureOperatorControllerBindingLevel.accept(xbox.y(), Superstructure.State.L4); configureOperatorControllerBindingLevel.accept( @@ -599,31 +699,8 @@ private void configureOperatorControllerBindings( .and(DriverStation::isTeleopEnabled) .whileTrue(hang.run(() -> hang.set(hangSpeed.getAsDouble())).finallyDo(hang::stop)); - Function rumble = - (rumbleType) -> - Commands.runEnd( - () -> xbox.setRumble(rumbleType, 1), () -> xbox.setRumble(rumbleType, 0)) - .withTimeout(0.2); - - if (hangSetpoints) { - xbox.rightBumper() - .debounce(0.1) - .and(xbox.leftBumper().negate().debounce(0.1)) - .and(anyButton.negate()) - .onTrue(hang.retract().andThen(rumble.apply(RumbleType.kRightRumble))); - xbox.leftBumper() - .debounce(0.1) - .and(xbox.rightBumper().negate().debounce(0.1)) - .and(anyButton.negate()) - .onTrue(hang.deploy().andThen(rumble.apply(RumbleType.kLeftRumble))); - xbox.rightBumper() - .and(xbox.leftBumper()) - .and(anyButton.negate()) - .onTrue(hang.stow().andThen(rumble.apply(RumbleType.kBothRumble))); - } else { - xbox.rightBumper().whileTrue(hang.runSet(-1)); - xbox.leftBumper().whileTrue(hang.runSet(+1)); - } + xbox.rightBumper().whileTrue(hang.runSet(-1)); + xbox.leftBumper().whileTrue(hang.runSet(+1)); } private Command rumbleController(CommandXboxController controller, double rumbleIntensity) { @@ -661,6 +738,17 @@ private void configureAlertTriggers() { RobotModeTriggers.autonomous() .and(isMatch) .onTrue(Commands.runOnce(() -> Elastic.selectTab("Autonomous"))); + + RobotModeTriggers.autonomous() + .whileTrue( + ledSubsystem.applyColor( + BlinkenLEDPattern.HEARTBEAT_BLUE, + BlinkenLEDPattern.HEARTBEAT_RED, + BlinkenLEDPattern.HEARTBEAT_WHITE)); + + ledSubsystem.setDefaultCommand( + ledSubsystem.applyColor( + BlinkenLEDPattern.BLUE, BlinkenLEDPattern.RED, BlinkenLEDPattern.WHITE)); } private void registerNamedCommands(String name, Command command) { @@ -802,10 +890,6 @@ private void configureAutos(LoggedDashboardChooser dashboardChooser) { // } if (Constants.RUNNING_TEST_PLANS) { - dashboardChooser.addOption("[TEST] Stow Hang Arm", hang.stow()); - dashboardChooser.addOption("[TEST] Deploy Hang Arm", hang.deploy()); - dashboardChooser.addOption("[TEST] Retract Hang Arm", hang.retract()); - dashboardChooser.addOption( "[Characterization] Elevator Static Forward", elevator.staticCharacterization(0.02)); dashboardChooser.addOption( diff --git a/src/main/java/frc/robot/commands/controllers/HeadingController.java b/src/main/java/frc/robot/commands/controllers/HeadingController.java index 679bb93..a522fc1 100644 --- a/src/main/java/frc/robot/commands/controllers/HeadingController.java +++ b/src/main/java/frc/robot/commands/controllers/HeadingController.java @@ -9,6 +9,8 @@ import edu.wpi.first.math.util.Units; import frc.robot.Constants; import frc.robot.subsystems.drive.Drive; +import frc.robot.utility.tunable.LoggedTunableNumber; +import frc.robot.utility.tunable.LoggedTunableNumberFactory; import org.littletonrobotics.junction.Logger; /** Controller for rotating robot to goal heading using ProfiledPIDController */ @@ -16,6 +18,24 @@ public class HeadingController { private final Drive drive; + private static final LoggedTunableNumberFactory factory = + new LoggedTunableNumberFactory("HeadingController/"); + + private static final LoggedTunableNumber kP = + factory.getNumber("kP", HEADING_CONTROLLER_CONFIG.pid().kP()); + + private static final LoggedTunableNumber kI = + factory.getNumber("kI", HEADING_CONTROLLER_CONFIG.pid().kI()); + + private static final LoggedTunableNumber kD = + factory.getNumber("kD", HEADING_CONTROLLER_CONFIG.pid().kD()); + + private static final LoggedTunableNumber kAngularVelocity = + factory.getNumber("kAngularVelocity", DRIVE_CONFIG.maxAngularVelocity()); + + private static final LoggedTunableNumber kAngularAcceleration = + factory.getNumber("kAngularAcceleration", DRIVE_CONFIG.maxAngularAcceleration()); + private final ProfiledPIDController headingControllerRadians; /** @@ -37,11 +57,10 @@ public HeadingController(Drive drive, double toleranceDegrees) { headingControllerRadians = new ProfiledPIDController( - HEADING_CONTROLLER_CONFIG.pid().kP(), - HEADING_CONTROLLER_CONFIG.pid().kI(), - HEADING_CONTROLLER_CONFIG.pid().kD(), - new TrapezoidProfile.Constraints( - DRIVE_CONFIG.maxAngularVelocity(), DRIVE_CONFIG.maxAngularAcceleration()), + kP.get(), + kI.get(), + kD.get(), + new TrapezoidProfile.Constraints(kAngularVelocity.get(), kAngularAcceleration.get()), Constants.LOOP_PERIOD_SECONDS); headingControllerRadians.enableContinuousInput(-Math.PI, Math.PI); @@ -96,16 +115,37 @@ public double calculate(Rotation2d goalHeadingRadians) { * @return rotation speed to reach heading goal, omega radians per second */ public double calculate() { + + // Update PID values + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> { + headingControllerRadians.setPID(values[0], values[1], values[2]); + }, + kP, + kI, + kD); + + // Update motion profile constraints + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> { + headingControllerRadians.setConstraints( + new TrapezoidProfile.Constraints(values[0], values[1])); + }, + kAngularVelocity, + kAngularAcceleration); + // Calculate output double measurement = drive.getRobotPose().getRotation().getRadians(); double output = headingControllerRadians.calculate(measurement); + // Record data + Logger.recordOutput("HeadingController/Goal", headingControllerRadians.getGoal().position); + Logger.recordOutput("HeadingController/Output", output); Logger.recordOutput( - "Drive/HeadingController/Goal", headingControllerRadians.getGoal().position); - Logger.recordOutput("Drive/HeadingController/Output", output); - Logger.recordOutput( - "Drive/HeadingController/HeadingError", headingControllerRadians.getPositionError()); - Logger.recordOutput("Drive/HeadingController/AtGoal", headingControllerRadians.atGoal()); + "HeadingController/HeadingError", headingControllerRadians.getPositionError()); + Logger.recordOutput("HeadingController/AtGoal", headingControllerRadians.atGoal()); return output; } diff --git a/src/main/java/frc/robot/commands/visionDemo/AimAtTagMode.java b/src/main/java/frc/robot/commands/visionDemo/AimAtTagMode.java new file mode 100644 index 0000000..60e9eca --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/AimAtTagMode.java @@ -0,0 +1,59 @@ +package frc.robot.commands.visionDemo; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import frc.robot.commands.visionDemo.SuperstructureUtil.SuperstructureState; +import frc.robot.commands.visionDemo.VisionDemoCommand.ResultSaftyMode; +import frc.robot.commands.visionDemo.VisionDemoCommand.VisionDemoResult; +import frc.robot.commands.visionDemo.filters.ComboAngleFilter; +import java.util.Optional; +import java.util.function.BooleanSupplier; + +public class AimAtTagMode implements VisionDemoCommand.VisionDemoMode { + + private static final Rotation2d TARGET_HEADING_OFFSET = Rotation2d.k180deg; + + private final ComboAngleFilter angleFilter = new ComboAngleFilter(3, 5); + + private final BooleanSupplier followWithSuperstructure; + + public AimAtTagMode(BooleanSupplier followWithSuperstructure) { + this.followWithSuperstructure = followWithSuperstructure; + } + + @Override + public ResultSaftyMode getExspectedSaftyMode() { + return ResultSaftyMode.ROTATIONAL_CONTROL; + } + + @Override + public Pose2d getRawPose(Pose2d robotPose, Pose3d tagPose) { + Rotation2d angle = + tagPose + .toPose2d() + .getTranslation() + .minus(robotPose.getTranslation()) + .getAngle() + .plus(TARGET_HEADING_OFFSET); + + return new Pose2d(robotPose.getTranslation(), angle); + } + + @Override + public VisionDemoResult calculate(Pose2d robotPose, Pose3d tagPose, double dt) { + Pose2d rawPose = getRawPose(robotPose, tagPose); + return new VisionDemoResult( + Optional.of( + new Pose2d(rawPose.getTranslation(), angleFilter.calculate(rawPose.getRotation()))), + ResultSaftyMode.ROTATIONAL_CONTROL); + } + + @Override + public Optional getSuperstructureState(Pose2d robotPose, Pose3d tagPose3d) { + if (!followWithSuperstructure.getAsBoolean()) { + return Optional.empty(); + } + return Optional.of(SuperstructureUtil.pointAtTag(tagPose3d)); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/ContainmentBox.java b/src/main/java/frc/robot/commands/visionDemo/ContainmentBox.java new file mode 100644 index 0000000..0300669 --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/ContainmentBox.java @@ -0,0 +1,120 @@ +package frc.robot.commands.visionDemo; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import frc.robot.utility.VirtualSubsystem; +import frc.robot.utility.tunable.LoggedTunableNumber; +import frc.robot.utility.tunable.LoggedTunableNumberFactory; +import java.util.Arrays; +import org.littletonrobotics.junction.Logger; + +/** Virtual subsystem for logging/displaying the state of the Safety box. */ +public class ContainmentBox extends VirtualSubsystem { + + private final Translation2d center; + private final Translation2d containedObjectSzie; + + private Translation2d bottomLeft; + private Translation2d topRight; + + private final String name; + + private final LoggedTunableNumberFactory tunableFactory; + private final LoggedTunableNumber lengthTunable; + private final LoggedTunableNumber widthTunable; + + public ContainmentBox( + String name, + Translation2d center, + double length, + double width, + Translation2d containedObjectSize) { + this.name = name; + this.center = center; + this.containedObjectSzie = containedObjectSize; + + this.tunableFactory = new LoggedTunableNumberFactory("ContainmentBox/" + name); + + bottomLeft = new Translation2d(center.getX() - length / 2.0, center.getY() - width / 2.0); + + topRight = new Translation2d(center.getX() + length / 2.0, center.getY() + width / 2.0); + + this.lengthTunable = tunableFactory.getNumber("Length", length); + this.widthTunable = tunableFactory.getNumber("Width", width); + } + + @Override + public void periodic() { + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> { + bottomLeft = + new Translation2d(center.getX() - values[0] / 2.0, center.getY() - values[1] / 2.0); + topRight = + new Translation2d(center.getX() + values[0] / 2.0, center.getY() + values[1] / 2.0); + }, + lengthTunable, + widthTunable); + + Logger.recordOutput("ContainmentBox/" + name + "/Outline", getOutline()); + Logger.recordOutput("ContainmentBox/" + name + "/Cones", getCones()); + Logger.recordOutput("ContainmentBox/" + name + "/Center", center); + } + + public Translation2d clamp(Translation2d point) { + double clampedX = + Math.min( + Math.max(point.getX(), bottomLeft.getX() + containedObjectSzie.getX() / 2.0), + topRight.getX() - containedObjectSzie.getX() / 2.0); + double clampedY = + Math.min( + Math.max(point.getY(), bottomLeft.getY() + containedObjectSzie.getY() / 2.0), + topRight.getY() - containedObjectSzie.getY() / 2.0); + return new Translation2d(clampedX, clampedY); + } + + public Pose2d clamp(Pose2d pose) { + return new Pose2d(clamp(pose.getTranslation()), pose.getRotation()); + } + + public boolean contains(Translation2d point) { + return point.getX() >= bottomLeft.getX() + && point.getX() <= topRight.getX() + && point.getY() >= bottomLeft.getY() + && point.getY() <= topRight.getY(); + } + + public boolean contains(Pose2d pose) { + return contains(pose.getTranslation()); + } + + private final Pose3d[] getCones() { + Rotation3d rotation = new Rotation3d(0, -Math.PI / 2, 0); + return Arrays.stream(getCorners()) + .map(corner -> new Pose3d(new Translation3d(corner.getTranslation()), rotation)) + .toArray(Pose3d[]::new); + } + + private Pose2d[] getOutline() { + return new Pose2d[] { + new Pose2d(bottomLeft, Rotation2d.kZero), + new Pose2d(new Translation2d(topRight.getX(), bottomLeft.getY()), Rotation2d.kZero), + new Pose2d(topRight, Rotation2d.kZero), + new Pose2d(new Translation2d(bottomLeft.getX(), topRight.getY()), Rotation2d.kZero), + new Pose2d(bottomLeft, Rotation2d.kZero), + }; + } + + public Pose2d[] getCorners() { + return new Pose2d[] { + new Pose2d(bottomLeft, Rotation2d.kZero), + new Pose2d(new Translation2d(topRight.getX(), bottomLeft.getY()), Rotation2d.kZero), + new Pose2d(topRight, Rotation2d.kZero), + new Pose2d(new Translation2d(bottomLeft.getX(), topRight.getY()), Rotation2d.kZero), + }; + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/FollowTagMode.java b/src/main/java/frc/robot/commands/visionDemo/FollowTagMode.java new file mode 100644 index 0000000..e7418dc --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/FollowTagMode.java @@ -0,0 +1,138 @@ +package frc.robot.commands.visionDemo; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.filter.Debouncer; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; +import frc.robot.commands.visionDemo.SuperstructureUtil.SuperstructureState; +import frc.robot.commands.visionDemo.VisionDemoCommand.ResultSaftyMode; +import frc.robot.commands.visionDemo.VisionDemoCommand.VisionDemoMode; +import frc.robot.commands.visionDemo.VisionDemoCommand.VisionDemoResult; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import org.littletonrobotics.junction.Logger; + +public class FollowTagMode implements VisionDemoMode { + + private static final Rotation2d TARGET_HEADING_OFFSET = Rotation2d.k180deg; + + private static final double FILTER_SLEW_RATE = Units.feetToMeters(6); + + private static final double MAX_TAG_JUMP = Units.feetToMeters(1); + private static final double MAX_TAG_JUMP_NEW_POSITION = Units.feetToMeters(3); + + private static final Debouncer timeTillActive = new Debouncer(0.5); + + private final Translation2d targetOffset; + + private Translation3d tagFilteredPosition; + + private BooleanSupplier followWithSuperstructure; + + public FollowTagMode(Translation2d targetOffset, BooleanSupplier followWithSuperstructure) { + this.followWithSuperstructure = followWithSuperstructure; + this.targetOffset = targetOffset; + } + + @Override + public void reset() { + tagFilteredPosition = null; + timeTillActive.calculate(false); + } + + @Override + public ResultSaftyMode getExspectedSaftyMode() { + return ResultSaftyMode.FULL_CONTROL; + } + + @Override + public Pose2d getRawPose(Pose2d robotPose, Pose3d tagPose) { + Rotation2d targetRotation = + tagPose.getRotation().toRotation2d().plus(Rotation2d.k180deg).plus(TARGET_HEADING_OFFSET); + + Translation2d targetTranslation = + tagPose.toPose2d().plus(new Transform2d(targetOffset, Rotation2d.kZero)).getTranslation(); + + return new Pose2d(targetTranslation, targetRotation); + } + + @Override + public VisionDemoResult calculate(Pose2d robotPose, Pose3d tagPose, double dt) { + + boolean isNewPosition = + tagFilteredPosition == null + || tagFilteredPosition.getDistance(tagPose.getTranslation()) + > MAX_TAG_JUMP_NEW_POSITION; + boolean isActive = timeTillActive.calculate(!isNewPosition); + + Logger.recordOutput("TagFollowing/Follow/IsNewPosition", isNewPosition); + Logger.recordOutput("TagFollowing/Follow/IsActive", isActive); + + if (tagFilteredPosition == null) { + tagFilteredPosition = tagPose.getTranslation(); + } + + tagFilteredPosition = + MathUtil.slewRateLimit(tagFilteredPosition, tagPose.getTranslation(), dt, FILTER_SLEW_RATE); + + if (!isActive) { + return new VisionDemoResult(Optional.empty(), ResultSaftyMode.UNKNOWN); + } + + Pose2d target = getRawPose(robotPose, tagPose); + + double distance = tagFilteredPosition.getDistance(tagPose.getTranslation()); + boolean withinMaxJump = distance < MAX_TAG_JUMP; + + Logger.recordOutput("TagFollowing/Follow/TagDistanceToSafe", distance); + Logger.recordOutput("TagFollowing/Follow/WithinMaxJump", withinMaxJump); + Logger.recordOutput( + "TagFollowing/Follow/TagFilteredPosition", + new Pose3d(this.tagFilteredPosition, tagPose.getRotation())); + + if (!withinMaxJump) { + return new VisionDemoResult(Optional.empty(), ResultSaftyMode.FULL_CONTROL); + } + + return new VisionDemoResult(Optional.of(target), ResultSaftyMode.FULL_CONTROL); + } + + @Override + public VisionDemoResult calculate( + Pose2d robotPose, Pose3d tagPose, double dt, ContainmentBox box) { + VisionDemoResult result = calculate(robotPose, tagPose, dt); + + return new VisionDemoResult( + result + .setpointPose() + .map( + pose -> + box.contains(pose) + ? pose + : new Pose2d( + box.clamp(pose.getTranslation()), + aimAtPose(robotPose, tagPose.toPose2d()))), + result.saftyMode()); + } + + private static Rotation2d aimAtPose(Pose2d robotPose, Pose2d tagPose) { + return tagPose + .getTranslation() + .minus(robotPose.getTranslation()) + .getAngle() + .plus(TARGET_HEADING_OFFSET); + } + + @Override + public Optional getSuperstructureState(Pose2d robotPose, Pose3d tagPose3d) { + if (!followWithSuperstructure.getAsBoolean()) { + return Optional.empty(); + } + return Optional.of(SuperstructureUtil.pointAtTag(tagPose3d)); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/SuperstructureUtil.java b/src/main/java/frc/robot/commands/visionDemo/SuperstructureUtil.java new file mode 100644 index 0000000..95e590b --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/SuperstructureUtil.java @@ -0,0 +1,38 @@ +package frc.robot.commands.visionDemo; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.util.Units; +import frc.robot.subsystems.superstructure.SuperstructureVisualizer; +import frc.robot.subsystems.superstructure.elevator.ElevatorConstants; +import frc.robot.subsystems.superstructure.wrist.WristConstants; + +public class SuperstructureUtil { + public record SuperstructureState(double elevatorHeight, Rotation2d wristAngle) {} + + public static final int mode = 1; + + public static final double ELEVATOR_HEIGHT_OFF_GROUND = + switch (mode) { + case 0 -> ElevatorConstants.elevatorDistanceFromGround + ElevatorConstants.carriageHeight; + case 1 -> ElevatorConstants.elevatorDistanceFromGround + + SuperstructureVisualizer.ROOT_Y + + ElevatorConstants.carriageHeight; + default -> Units.inchesToMeters(12); + }; + + public static final double WRIST_LENGTH = + switch (mode) { + case 0, 1 -> WristConstants.WRIST_LENGTH; + default -> Units.inchesToMeters(18); + }; + + public static SuperstructureState pointAtTag(Pose3d tagPose) { + final Rotation2d wristAngle = new Rotation2d(tagPose.getRotation().getY()); + final double elevatorHeight = + tagPose.getTranslation().getZ() + - WRIST_LENGTH * wristAngle.getSin() + - ELEVATOR_HEIGHT_OFF_GROUND; + return new SuperstructureState(elevatorHeight, wristAngle); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/TagFollowingVision.java b/src/main/java/frc/robot/commands/visionDemo/TagFollowingVision.java new file mode 100644 index 0000000..fffdb32 --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/TagFollowingVision.java @@ -0,0 +1,138 @@ +package frc.robot.commands.visionDemo; + +import edu.wpi.first.math.filter.Debouncer; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Quaternion; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import frc.robot.subsystems.vision.AprilTagVision; +import frc.robot.subsystems.vision.Camera.AbsoluteTrackedTarget; +import frc.robot.utility.VirtualSubsystem; +import java.util.List; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import org.littletonrobotics.junction.Logger; + +public class TagFollowingVision extends VirtualSubsystem { + + public static BooleanSupplier debugModeSupplier = () -> false; + + private static final double MAX_POSE_AMBIGUITY = 0.2; + private static final double MAX_PITCH_ANGLE = Units.degreesToRadians(70); + + private final AprilTagVision vision; + private final int tagId; + + private final Debouncer hasTargetDebouncer = new Debouncer(0.3); + private boolean isTracking = false; + + private Optional lastTagPose = Optional.empty(); + + public TagFollowingVision(AprilTagVision vision, int tagId) { + this.vision = vision; + this.tagId = tagId; + } + + @Override + public void periodic() { + List tags = + vision.getAbsoluteTrackedTargets().stream().filter(tag -> tag.id() == tagId).toList(); + + if (!tags.isEmpty()) { + Logger.recordOutput( + "TagTracking/Tags/MaxPoseAmbiguity", + tags.stream().mapToDouble(AbsoluteTrackedTarget::poseAmbiguity).max().orElse(-1)); + } + + List filteredTags = + tags.stream() + .filter(tag -> tag.poseAmbiguity() < MAX_POSE_AMBIGUITY) + .filter(tag -> tag.targetPose().getTranslation().getZ() > 0) // Below floor + .filter( + tag -> isPointingAtPose(tag.robotPose(), tag.targetPose().toPose2d(), Math.PI / 2)) + .filter(tag -> Math.abs(tag.targetPose().getRotation().getY()) < MAX_PITCH_ANGLE) + .toList(); + + Logger.recordOutput("TagTracking/TotalDetected", tags.size()); + Logger.recordOutput("TagTracking/TotalFiltered", filteredTags.size()); + + boolean noTargets = filteredTags.isEmpty(); + boolean noTargetsStable = hasTargetDebouncer.calculate(noTargets); + + this.isTracking = !noTargetsStable; + SmartDashboard.putBoolean("Is Tracking Tag?", isTracking); + + Logger.recordOutput("TagTracking/noTargets", noTargets); + Logger.recordOutput("TagTracking/noTargetsStable", noTargetsStable); + + if (!noTargets || noTargetsStable) { + Logger.recordOutput( + "TagTracking/Tags/Pose3d", + filteredTags.stream().map(AbsoluteTrackedTarget::targetPose).toArray(Pose3d[]::new)); + Logger.recordOutput( + "TagTracking/Tags/CameraPoses3d", + filteredTags.stream().map(AbsoluteTrackedTarget::cameraPose).toArray(Pose3d[]::new)); + } + + if (!noTargets) { + Pose3d averageTagPose = + meanPose(filteredTags.stream().map(AbsoluteTrackedTarget::targetPose).toList()); + Logger.recordOutput("TagTracking/Target/Pose3d", averageTagPose); + lastTagPose = Optional.of(averageTagPose); + } else { + lastTagPose = Optional.empty(); + } + } + + public boolean isTracking() { + return isTracking; + } + + public Optional getLastTagPose() { + return lastTagPose; + } + + private static boolean isPointingAtPose(Pose2d robotPose, Pose2d targetPose, double maxAngle) { + Rotation2d angleToFaceTag = + robotPose.getTranslation().minus(targetPose.getTranslation()).getAngle(); + Rotation2d angleOfTag = targetPose.getRotation(); + double angleDiff = angleToFaceTag.minus(angleOfTag).getRadians(); + return Math.abs(angleDiff) < maxAngle; + } + + private static Pose3d meanPose(List poses) { + if (poses.isEmpty()) { + throw new IllegalArgumentException("Cannot average an empty list of poses"); + } + + if (poses.size() == 1) { + return poses.get(0); + } + + Translation3d summedTranslations = new Translation3d(0, 0, 0); + Quaternion summmedQuaternion = new Quaternion(0, 0, 0, 0); + + Quaternion reference = poses.get(0).getRotation().getQuaternion(); + + for (Pose3d pose : poses) { + summedTranslations = summedTranslations.plus(pose.getTranslation()); + + Quaternion q = pose.getRotation().getQuaternion(); + + if (q.dot(reference) < 0) { + q = q.times(-1); + } + + summmedQuaternion = summmedQuaternion.plus(q); + } + + Translation3d avgTranslations = summedTranslations.div(poses.size()); + Quaternion avgQuaternion = summmedQuaternion.divide(poses.size()).normalize(); + + return new Pose3d(avgTranslations, new Rotation3d(avgQuaternion)); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/VisionDemoCommand.java b/src/main/java/frc/robot/commands/visionDemo/VisionDemoCommand.java new file mode 100644 index 0000000..47943e5 --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/VisionDemoCommand.java @@ -0,0 +1,277 @@ +package frc.robot.commands.visionDemo; + +import edu.wpi.first.math.filter.Debouncer; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import edu.wpi.first.wpilibj2.command.Command; +import frc.robot.commands.controllers.SimpleDriveController; +import frc.robot.commands.visionDemo.SuperstructureUtil.SuperstructureState; +import frc.robot.commands.visionDemo.filters.ComboAngleFilter; +import frc.robot.commands.visionDemo.filters.ComboFilter; +import frc.robot.commands.visionDemo.filters.MeanPoseFilterTimeBased; +import frc.robot.subsystems.drive.Drive; +import frc.robot.subsystems.led.BlinkenLEDPattern; +import frc.robot.subsystems.led.LEDSubsystem; +import frc.robot.subsystems.superstructure.elevator.Elevator; +import frc.robot.subsystems.superstructure.wrist.Wrist; +import java.util.Optional; +import org.littletonrobotics.junction.Logger; + +public class VisionDemoCommand extends Command { + + public interface VisionDemoMode { + + public default void reset() {} + + public ResultSaftyMode getExspectedSaftyMode(); + + public Pose2d getRawPose(Pose2d robotPose, Pose3d tagPose); + + public VisionDemoResult calculate(Pose2d robotPose, Pose3d tagPose, double dt); + + public default VisionDemoResult calculate( + Pose2d robotPose, Pose3d tagPose, double dt, ContainmentBox box) { + return calculate(robotPose, tagPose, dt); + } + + public Optional getSuperstructureState(Pose2d robotPose, Pose3d tagPose3d); + } + + public record VisionDemoResult(Optional setpointPose, ResultSaftyMode saftyMode) {} + + public enum ResultSaftyMode { + FULL_CONTROL, + ROTATIONAL_CONTROL, + STOP, + UNKNOWN; + } + + private final Drive drive; + private final TagFollowingVision vision; + + private final Elevator elevator; + private final Wrist wrist; + + private final LEDSubsystem leds; + + private final VisionDemoMode mode; + + private final ContainmentBox box; + + private final SimpleDriveController controller = new SimpleDriveController(); + private final Debouncer atGoalDebouncer = new Debouncer(0.2); + + private final ComboFilter elevatorHeightFilter = new ComboFilter(3, 5); + private final ComboAngleFilter wristAngleFilter = new ComboAngleFilter(3, 5); + + private ResultSaftyMode saftyMode = ResultSaftyMode.STOP; + + private final Timer deltaTimeTimer = new Timer(); + + private final MeanPoseFilterTimeBased setpointStablityCheckingFilter = + new MeanPoseFilterTimeBased(0.6); + + private final ComboFilter vxLimiter = new ComboFilter(10, 3); // m/s^2 + private final ComboFilter vyLimiter = new ComboFilter(10, 3); // m/s^2 + private final ComboFilter omegaLimiter = new ComboFilter(10, 3); // rad/s^2 + // private final SlewRateLimiter vxLimiter = new SlewRateLimiter(3); // m/s^2 + // private final SlewRateLimiter vyLimiter = new SlewRateLimiter(3); // m/s^2 + // private final SlewRateLimiter omegaLimiter = new SlewRateLimiter(3); // rad/s^2 + + public VisionDemoCommand( + Drive drive, + TagFollowingVision vision, + Elevator elevator, + Wrist wrist, + LEDSubsystem leds, + VisionDemoMode mode, + ContainmentBox box) { + this.drive = drive; + this.vision = vision; + this.elevator = elevator; + this.wrist = wrist; + this.leds = leds; + this.mode = mode; + this.box = box; + + controller.setTolerance( + new Pose2d(Units.inchesToMeters(4), Units.inchesToMeters(4), Rotation2d.fromDegrees(5))); + + addRequirements(drive, elevator, wrist, leds); + } + + @Override + public void initialize() { + controller.reset(drive.getRobotPose()); + controller.setSetpoint(drive.getRobotPose()); + mode.reset(); + deltaTimeTimer.restart(); + saftyMode = ResultSaftyMode.UNKNOWN; + Logger.recordOutput("TagFollowing/Box/Coners", box.getCorners()); + } + + @Override + public void execute() { + Pose2d robotPose = drive.getRobotPose(); + + // --- Target Logging --- + + Logger.recordOutput("TagFollowing/Result/SaftyMode", saftyMode.toString()); + updateLEDS(vision.isTracking()); + + vision + .getLastTagPose() + .ifPresent( + tagPose -> { + Pose2d rawSetpointPose = mode.getRawPose(robotPose, tagPose); + Logger.recordOutput("TagFollowing/RobotRawSetpointPose", rawSetpointPose); + SmartDashboard.putNumber( + "Target Heading", -rawSetpointPose.getRotation().getDegrees()); + + double dt = deltaTimeTimer.get(); + Logger.recordOutput("TagFollowing/DeltaTime", dt); + deltaTimeTimer.restart(); + + VisionDemoResult setpointPose = mode.calculate(robotPose, tagPose, dt, box); + saftyMode = setpointPose.saftyMode(); + + mode.getSuperstructureState(robotPose, tagPose).ifPresent(this::updateSuperstructure); + + setpointPose + .setpointPose() + .ifPresent( + pose -> { + Pose2d clampedPose = box.clamp(pose); + controller.setSetpoint(clampedPose); + + setpointStablityCheckingFilter.calculate( + clampedPose, Timer.getFPGATimestamp()); + + Logger.recordOutput("TagFollowing/Box/RobotUnclampedSetpointPose", pose); + Logger.recordOutput("TagFollowing/RobotSetpointPose", clampedPose); + Logger.recordOutput( + "TagFollowing/Box/SetpointPoseInBox", box.contains(pose)); + }); + }); + + Logger.recordOutput("TagFollowing/Box/CurrentPoseInBox", box.contains(robotPose)); + + ChassisSpeeds speeds = getChassisSpeeds(); + drive.setRobotSpeeds(speeds); + } + + private void updateLEDS(boolean isUpdating) { + + ResultSaftyMode saftyMode = this.saftyMode; + + if (saftyMode == ResultSaftyMode.UNKNOWN) { + saftyMode = mode.getExspectedSaftyMode(); + } + + BlinkenLEDPattern pattern = + switch (saftyMode) { + case FULL_CONTROL -> isUpdating ? BlinkenLEDPattern.CHASE_RED : BlinkenLEDPattern.ORANGE; + case ROTATIONAL_CONTROL -> isUpdating + ? BlinkenLEDPattern.CHASE_BLUE + : BlinkenLEDPattern.GREEN; + case STOP -> BlinkenLEDPattern.WHITE; + case UNKNOWN -> BlinkenLEDPattern.RED; + }; + + Logger.recordOutput("TagFollowing/LEDPattern", pattern.toString()); + + leds.set(pattern); + } + + private void updateSuperstructure(SuperstructureState state) { + double filteredElevatorHeight = elevatorHeightFilter.calculate(state.elevatorHeight()); + Rotation2d filteredWristAngle = wristAngleFilter.calculate(state.wristAngle()); + + elevator.setGoalHeightMeters(filteredElevatorHeight); + wrist.setGoalRotation(filteredWristAngle); + + Logger.recordOutput( + "TagFollowing/Superstructure/DesiredElevatorHeight", filteredElevatorHeight); + Logger.recordOutput( + "TagFollowing/Superstructure/DesiredWristAngle", filteredWristAngle.getDegrees()); + } + + private ChassisSpeeds getChassisSpeeds() { + ChassisSpeeds speeds = controller.calculate(drive.getRobotPose()); + + boolean atReference = controller.atReference(); + boolean atReferenceStable = atGoalDebouncer.calculate(atReference); + + Logger.recordOutput("TagFollowing/AtReference", atReference); + Logger.recordOutput("TagFollowing/AtReferenceStable", atReferenceStable); + + boolean stopped = false; + + if (atReferenceStable) { + speeds = new ChassisSpeeds(0, 0, 0); + } + + switch (saftyMode) { + case FULL_CONTROL: + // Do nothing, full control + break; + case ROTATIONAL_CONTROL: + speeds.vxMetersPerSecond = 0; + speeds.vyMetersPerSecond = 0; + break; + case STOP, UNKNOWN: + speeds = new ChassisSpeeds(0, 0, 0); + stopped = true; + break; + } + + ChassisSpeeds filteredSPeeds = + new ChassisSpeeds( + vxLimiter.calculate(speeds.vxMetersPerSecond), + vyLimiter.calculate(speeds.vyMetersPerSecond), + omegaLimiter.calculate(speeds.omegaRadiansPerSecond)); + + Logger.recordOutput("TagFollowing/ChassisSpeeds/Speeds", speeds); + Logger.recordOutput("TagFollowing/ChassisSpeeds/FilteredSpeeds", filteredSPeeds); + + boolean isSafeToAntiJitter = !stopped && box.contains(drive.getRobotPose()); + + boolean hasStableTarget = + setpointStablityCheckingFilter.lastValue() == null + || setpointStablityCheckingFilter + .calculateMean() + .getTranslation() + .getDistance(setpointStablityCheckingFilter.lastValue().getTranslation()) + < Units.inchesToMeters(6); + + Logger.recordOutput("TagFollowing/ChassisSpeeds/IsSafeToAntiJitter", isSafeToAntiJitter); + Logger.recordOutput("TagFollowing/ChassisSpeeds/hasStableTarget", hasStableTarget); + + boolean isAntiJitter = isSafeToAntiJitter && !hasStableTarget; + Logger.recordOutput("TagFollowing/ChassisSpeeds/IsAntiJitter", isAntiJitter); + + if (isAntiJitter) { + return filteredSPeeds; + } + + return speeds; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public void end(boolean interrupted) { + SmartDashboard.putBoolean("Has Track Target", false); + SmartDashboard.putNumber("Target Heading", Double.NaN); + Logger.recordOutput("TagFollowing/Result/SaftyMode", ResultSaftyMode.UNKNOWN.toString()); + drive.stop(); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/ComboAngleFilter.java b/src/main/java/frc/robot/commands/visionDemo/filters/ComboAngleFilter.java new file mode 100644 index 0000000..e27a7ba --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/ComboAngleFilter.java @@ -0,0 +1,27 @@ +package frc.robot.commands.visionDemo.filters; + +import edu.wpi.first.math.geometry.Rotation2d; + +public class ComboAngleFilter { + private final MedianAngleFilter medianFilter; + private final MeanAngleFilter meanFilter; + + public ComboAngleFilter(int medianSize, int meanSize) { + medianFilter = new MedianAngleFilter(medianSize); + meanFilter = new MeanAngleFilter(meanSize); + } + + public Rotation2d calculate(Rotation2d rawValue) { + Rotation2d withoutOutliers = medianFilter.calculate(rawValue); + Rotation2d mean = meanFilter.calculate(withoutOutliers); + // System.out.printf( + // "ComboAngleFilter: raw=%+.2f median=%+.2f mean=%+.2f%n", + // rawValue.getDegrees(), withoutOutliers.getDegrees(), mean.getDegrees()); + return mean; + } + + public void reset() { + medianFilter.reset(); + meanFilter.reset(); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/ComboFilter.java b/src/main/java/frc/robot/commands/visionDemo/filters/ComboFilter.java new file mode 100644 index 0000000..68c5627 --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/ComboFilter.java @@ -0,0 +1,23 @@ +package frc.robot.commands.visionDemo.filters; + +import edu.wpi.first.math.filter.MedianFilter; + +public class ComboFilter { + private final MedianFilter medianFilter; + private final MeanFilter meanFilter; + + public ComboFilter(int medianSize, int meanSize) { + medianFilter = new MedianFilter(medianSize); + meanFilter = new MeanFilter(meanSize); + } + + public double calculate(double rawValue) { + double withoutOutliers = medianFilter.calculate(rawValue); + return meanFilter.calculate(withoutOutliers); + } + + public void reset() { + medianFilter.reset(); + meanFilter.reset(); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/MeanAngleFilter.java b/src/main/java/frc/robot/commands/visionDemo/filters/MeanAngleFilter.java new file mode 100644 index 0000000..3e632fe --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/MeanAngleFilter.java @@ -0,0 +1,86 @@ +package frc.robot.commands.visionDemo.filters; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.util.DoubleCircularBuffer; + +/** + * A class that implements a moving-window mean filter for angles. Useful for reducing measurement + * noise by averaging angles over a sliding window while properly handling wrap-around. + */ +public class MeanAngleFilter { + private final DoubleCircularBuffer m_valueBuffer; + private final int m_size; + + /** + * Creates a new MeanAngleFilter. + * + * @param size The number of samples in the moving window. + */ + public MeanAngleFilter(int size) { + // Circular buffer of values currently in the window (stored as radians) + m_valueBuffer = new DoubleCircularBuffer(size); + // Size of rolling window + m_size = size; + } + + /** + * Calculates the moving-window mean for the next value of the input stream. + * + * @param next The next input value. + * @return The mean of the moving window, updated to include the next value. + */ + public Rotation2d calculate(Rotation2d next) { + // If buffer is at max size, remove oldest value + if (m_valueBuffer.size() >= m_size) { + m_valueBuffer.removeLast(); + } + + // Add next value to circular buffer + m_valueBuffer.addFirst(next.getRadians()); + + // Calculate mean using vector averaging + return calculateMean(); + } + + /** + * Calculates the mean angle using vector averaging. This properly handles wrap-around by + * converting angles to unit vectors, averaging them, and converting back. + */ + private Rotation2d calculateMean() { + int bufferSize = m_valueBuffer.size(); + if (bufferSize == 0) { + return Rotation2d.kZero; + } + + // Sum of x and y components of unit vectors + double sumX = 0.0; + double sumY = 0.0; + + for (int i = 0; i < bufferSize; i++) { + double angle = m_valueBuffer.get(i); + sumX += Math.cos(angle); + sumY += Math.sin(angle); + } + + // Average the components + double avgX = sumX / bufferSize; + double avgY = sumY / bufferSize; + + // Convert back to angle using atan2 + return new Rotation2d(avgX, avgY); + } + + /** + * Returns the last value added to the MeanAngleFilter. + * + * @return The last value. + */ + public Rotation2d lastValue() { + return new Rotation2d(m_valueBuffer.getFirst()); + } + + /** Resets the filter, clearing the window of all elements. */ + public void reset() { + m_valueBuffer.clear(); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/MeanFilter.java b/src/main/java/frc/robot/commands/visionDemo/filters/MeanFilter.java new file mode 100644 index 0000000..f01a08d --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/MeanFilter.java @@ -0,0 +1,62 @@ +package frc.robot.commands.visionDemo.filters; + +import edu.wpi.first.util.DoubleCircularBuffer; + +/** + * A class that implements a moving-window mean filter. Useful for reducing measurement noise by + * averaging values over a sliding window. + */ +public class MeanFilter { + private final DoubleCircularBuffer m_valueBuffer; + private final int m_size; + private double m_sum; + + /** + * Creates a new MeanFilter. + * + * @param size The number of samples in the moving window. + */ + public MeanFilter(int size) { + // Circular buffer of values currently in the window + m_valueBuffer = new DoubleCircularBuffer(size); + // Size of rolling window + m_size = size; + // Running sum of values in the window + m_sum = 0.0; + } + + /** + * Calculates the moving-window mean for the next value of the input stream. + * + * @param next The next input value. + * @return The mean of the moving window, updated to include the next value. + */ + public double calculate(double next) { + // If buffer is at max size, remove oldest value from sum + if (m_valueBuffer.size() >= m_size) { + m_sum -= m_valueBuffer.removeLast(); + } + + // Add next value to circular buffer and sum + m_valueBuffer.addFirst(next); + m_sum += next; + + // Return average + return m_sum / m_valueBuffer.size(); + } + + /** + * Returns the last value calculated by the MeanFilter. + * + * @return The last value. + */ + public double lastValue() { + return m_valueBuffer.getFirst(); + } + + /** Resets the filter, clearing the window of all elements. */ + public void reset() { + m_valueBuffer.clear(); + m_sum = 0.0; + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/MeanPoseFilter.java b/src/main/java/frc/robot/commands/visionDemo/filters/MeanPoseFilter.java new file mode 100644 index 0000000..e2de3ae --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/MeanPoseFilter.java @@ -0,0 +1,89 @@ +package frc.robot.commands.visionDemo.filters; + +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.util.CircularBuffer; + +/** A class that implements a moving-window mean filter for poses. */ +public class MeanPoseFilter { + private final CircularBuffer m_valueBuffer; + private final int m_size; + + /** + * Creates a new MeanAngleFilter. + * + * @param size The number of samples in the moving window. + */ + public MeanPoseFilter(int size) { + // Circular buffer of values currently in the window (stored as radians) + m_valueBuffer = new CircularBuffer<>(size); + // Size of rolling window + m_size = size; + } + + /** + * Calculates the moving-window mean for the next value of the input stream. + * + * @param next The next input value. + * @return The mean of the moving window, updated to include the next value. + */ + public Pose2d calculate(Pose2d next) { + // If buffer is at max size, remove oldest value + if (m_valueBuffer.size() >= m_size) { + m_valueBuffer.removeLast(); + } + + // Add next value to circular buffer + m_valueBuffer.addFirst(next); + + // Calculate mean using vector averaging + return calculateMean(); + } + + /** + * Calculates the mean angle using vector averaging. This properly handles wrap-around by + * converting angles to unit vectors, averaging them, and converting back. + */ + public Pose2d calculateMean() { + int bufferSize = m_valueBuffer.size(); + + if (bufferSize == 0) { + return null; + } + + // Sum of x and y components of unit vectors + double sumX = 0.0; + double sumY = 0.0; + Translation2d sumTranslation = new Translation2d(0, 0); + + for (int i = 0; i < bufferSize; i++) { + Rotation2d angle = m_valueBuffer.get(i).getRotation(); + sumX += angle.getCos(); + sumY += angle.getSin(); + sumTranslation = sumTranslation.plus(m_valueBuffer.get(i).getTranslation()); + } + + // Average the components + double avgX = sumX / bufferSize; + double avgY = sumY / bufferSize; + Translation2d avgTranslation = sumTranslation.div(bufferSize); + + // Convert back to angle using atan2 + return new Pose2d(avgTranslation, new Rotation2d(avgX, avgY)); + } + + /** + * Returns the last value added to the MeanAngleFilter. + * + * @return The last value. + */ + public Pose2d lastValue() { + return m_valueBuffer.getFirst(); + } + + /** Resets the filter, clearing the window of all elements. */ + public void reset() { + m_valueBuffer.clear(); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/MeanPoseFilterTimeBased.java b/src/main/java/frc/robot/commands/visionDemo/filters/MeanPoseFilterTimeBased.java new file mode 100644 index 0000000..31e9a06 --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/MeanPoseFilterTimeBased.java @@ -0,0 +1,119 @@ +package frc.robot.commands.visionDemo.filters; + +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.wpilibj.Timer; +import java.util.LinkedList; + +/** A class that implements a time-based moving-window mean filter for poses. */ +public class MeanPoseFilterTimeBased { + private static class TimestampedPose { + public final Pose2d pose; + public final double timestamp; + + public TimestampedPose(Pose2d pose, double timestamp) { + this.pose = pose; + this.timestamp = timestamp; + } + } + + private final LinkedList m_valueBuffer; + private final double m_windowDuration; + + /** + * Creates a new MeanPoseFilterTimeBased. + * + * @param windowDuration The duration of the time window in seconds. + */ + public MeanPoseFilterTimeBased(double windowDuration) { + m_valueBuffer = new LinkedList<>(); + m_windowDuration = windowDuration; + } + + /** + * Calculates the moving-window mean for the next value of the input stream. + * + * @param next The next input value. + * @return The mean of the moving window, updated to include the next value. + */ + public Pose2d calculate(Pose2d next) { + return calculate(next, Timer.getFPGATimestamp()); + } + + /** + * Calculates the moving-window mean for the next value of the input stream. + * + * @param next The next input value. + * @param timestamp The timestamp of this measurement in seconds. + * @return The mean of the moving window, updated to include the next value. + */ + public Pose2d calculate(Pose2d next, double timestamp) { + // Add new value to the front of the buffer + m_valueBuffer.addFirst(new TimestampedPose(next, timestamp)); + + // Remove old values outside the time window + double cutoffTime = timestamp - m_windowDuration; + while (!m_valueBuffer.isEmpty() && m_valueBuffer.getLast().timestamp < cutoffTime) { + m_valueBuffer.removeLast(); + } + + // Calculate mean using vector averaging + return calculateMean(); + } + + /** + * Calculates the mean pose using vector averaging for rotation. This properly handles wrap-around + * by converting angles to unit vectors, averaging them, and converting back. + */ + public Pose2d calculateMean() { + int bufferSize = m_valueBuffer.size(); + + if (bufferSize == 0) { + return null; + } + + // Sum of x and y components of unit vectors (for rotation) + double sumX = 0.0; + double sumY = 0.0; + Translation2d sumTranslation = new Translation2d(0, 0); + + for (TimestampedPose tp : m_valueBuffer) { + Rotation2d angle = tp.pose.getRotation(); + sumX += angle.getCos(); + sumY += angle.getSin(); + sumTranslation = sumTranslation.plus(tp.pose.getTranslation()); + } + + // Average the components + double avgX = sumX / bufferSize; + double avgY = sumY / bufferSize; + Translation2d avgTranslation = sumTranslation.div(bufferSize); + + // Convert back to angle using atan2 + return new Pose2d(avgTranslation, new Rotation2d(avgX, avgY)); + } + + /** + * Returns the last value added to the MeanPoseFilter. + * + * @return The last value. + */ + public Pose2d lastValue() { + return m_valueBuffer.isEmpty() ? null : m_valueBuffer.getFirst().pose; + } + + /** Resets the filter, clearing the window of all elements. */ + public void reset() { + m_valueBuffer.clear(); + } + + /** + * Returns the number of poses currently in the filter window. + * + * @return The number of poses in the buffer. + */ + public int size() { + return m_valueBuffer.size(); + } +} diff --git a/src/main/java/frc/robot/commands/visionDemo/filters/MedianAngleFilter.java b/src/main/java/frc/robot/commands/visionDemo/filters/MedianAngleFilter.java new file mode 100644 index 0000000..a84d1d7 --- /dev/null +++ b/src/main/java/frc/robot/commands/visionDemo/filters/MedianAngleFilter.java @@ -0,0 +1,111 @@ +package frc.robot.commands.visionDemo.filters; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.util.CircularBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * A class that implements a moving-window median filter for angles. Useful for rejecting outliers + * in angular measurements while properly handling angle wrapping. + */ +public class MedianAngleFilter { + private final CircularBuffer m_valueBuffer; + private final int m_size; + + /** + * Creates a new MedianAngleFilter. + * + * @param size The number of samples in the moving window. + */ + public MedianAngleFilter(int size) { + m_valueBuffer = new CircularBuffer<>(size); + m_size = size; + } + + /** + * Calculates the moving-window median for the next value of the input stream. + * + * @param next The next input value. + * @return The median of the moving window, updated to include the next value. + */ + public Rotation2d calculate(Rotation2d next) { + // If buffer is at max size, remove oldest value + if (m_valueBuffer.size() >= m_size) { + m_valueBuffer.removeLast(); + } + + // Add next value to circular buffer + m_valueBuffer.addFirst(next); + + // Return median + return calculateMedian(); + } + + /** + * Calculates the median angle from the current buffer using the "smallest arc" method. This + * method finds the reference angle that minimizes the sum of angular distances, then computes the + * median relative to that reference. + */ + private Rotation2d calculateMedian() { + int bufferSize = m_valueBuffer.size(); + if (bufferSize == 0) { + return Rotation2d.kZero; + } + if (bufferSize == 1) { + return m_valueBuffer.getFirst(); + } + + // Use the first angle as reference point + Rotation2d reference = m_valueBuffer.getFirst(); + + // Convert all angles to offsets from the reference + // The offsets will be in range [-pi, pi] + List offsets = new ArrayList<>(bufferSize); + for (int i = 0; i < bufferSize; i++) { + Rotation2d angle = m_valueBuffer.get(i); + double offset = angle.minus(reference).getRadians(); + offsets.add(offset); + } + + // Sort the offsets + offsets.sort(Comparator.naturalOrder()); + + // Find median offset + double medianOffset; + if (bufferSize % 2 == 1) { + medianOffset = offsets.get(bufferSize / 2); + } else { + // For even number of elements, average the two middle elements + // Need to be careful here if the two middle values are on opposite sides + double lower = offsets.get(bufferSize / 2 - 1); + double upper = offsets.get(bufferSize / 2); + + // If the gap between them is > pi, we're wrapping around + if (upper - lower > Math.PI) { + // Average by wrapping around the other way + medianOffset = lower + (upper - lower - 2 * Math.PI) / 2.0; + } else { + medianOffset = (lower + upper) / 2.0; + } + } + + // Convert back to absolute angle + return reference.plus(new Rotation2d(medianOffset)); + } + + /** + * Returns the last value added to the MedianAngleFilter. + * + * @return The last value. + */ + public Rotation2d lastValue() { + return m_valueBuffer.getFirst(); + } + + /** Resets the filter, clearing the window of all elements. */ + public void reset() { + m_valueBuffer.clear(); + } +} diff --git a/src/main/java/frc/robot/subsystems/dashboard/DriverDashboard.java b/src/main/java/frc/robot/subsystems/dashboard/DriverDashboard.java index 2ec2cb9..7928b3c 100644 --- a/src/main/java/frc/robot/subsystems/dashboard/DriverDashboard.java +++ b/src/main/java/frc/robot/subsystems/dashboard/DriverDashboard.java @@ -1,7 +1,5 @@ package frc.robot.subsystems.dashboard; -import edu.wpi.first.math.filter.Debouncer; -import edu.wpi.first.math.filter.Debouncer.DebounceType; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.kinematics.ChassisSpeeds; @@ -49,7 +47,6 @@ public static DriverDashboard getInstance() { private Supplier speedsSupplier; private BooleanSupplier hasVisionEstimate; - private Debouncer debouncer; private BooleanSupplier usingIntakeSensor; private BooleanSupplier hasCoral; @@ -58,6 +55,8 @@ public static DriverDashboard getInstance() { private DoubleSupplier hangValue; + private BooleanSupplier isInBox; + // --- Setters --- public void addSubsystem(SubsystemBase subsystem) { @@ -65,11 +64,11 @@ public void addSubsystem(SubsystemBase subsystem) { } public void addCommand(String name, Runnable runnable, boolean runsWhenDisabled) { - addCommand(name, Commands.runOnce(runnable), runsWhenDisabled); + addCommand(name, Commands.runOnce(runnable).ignoringDisable(runsWhenDisabled)); } - public void addCommand(String name, Command command, boolean runsWhenDisabled) { - SmartDashboard.putData(name, command.withName(name).ignoringDisable(runsWhenDisabled)); + public void addCommand(String name, Command command) { + SmartDashboard.putData(name, command.withName(name)); } public void setPoseSupplier(Supplier robotPoseSupplier) { @@ -92,9 +91,8 @@ public void setHeadingControlledSupplier(BooleanSupplier headingControlledSuppli this.headingControlledSupplier = headingControlledSupplier; } - public void setHasVisionEstimateSupplier(BooleanSupplier hasVisionEstimate, double debounceTime) { + public void setHasVisionEstimateSupplier(BooleanSupplier hasVisionEstimate) { this.hasVisionEstimate = hasVisionEstimate; - debouncer = new Debouncer(debounceTime, DebounceType.kFalling); } public void setSensorSuppliers(BooleanSupplier usingIntakeSensor, BooleanSupplier hasCoral) { @@ -110,6 +108,10 @@ public void setSuperstructureAtGoal(BooleanSupplier superstructureAtGoal) { this.superstructureAtGoal = superstructureAtGoal; } + public void setIsInBox(BooleanSupplier isInBox) { + this.isInBox = isInBox; + } + public Field2d getField() { return field; } @@ -120,20 +122,8 @@ public void periodic() { if (poseSupplier != null) { Pose2d pose = poseSupplier.get(); - SmartDashboard.putNumber("Heading Degrees", ((-pose.getRotation().getDegrees() + 360) % 360)); + SmartDashboard.putNumber("Heading Degrees", -pose.getRotation().getDegrees()); field.setRobotPose(pose); - - // SmartDashboard.putNumber( - // "Distance To Reef [Tag 8] [Test]", - // Units.metersToInches( - // Math.abs( - // AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeAndyMark) - // .getTagPose(18) - // .get() - // .toPose2d() - // .minus(pose) - // .getX()) - // - DRIVE_CONFIG.bumperCornerToCorner().getX() / 2.0)); } if (autoAlginPoseSupplier != null) { @@ -159,8 +149,7 @@ public void periodic() { } if (hasVisionEstimate != null) { - SmartDashboard.putBoolean( - "Has Vision", debouncer.calculate(hasVisionEstimate.getAsBoolean())); + SmartDashboard.putBoolean("Has Vision", hasVisionEstimate.getAsBoolean()); } if (usingIntakeSensor != null) { @@ -178,5 +167,9 @@ public void periodic() { if (hangValue != null) { SmartDashboard.putNumber("Hang Value", hangValue.getAsDouble()); } + + if (isInBox != null) { + SmartDashboard.putBoolean("Is In Box?", isInBox.getAsBoolean()); + } } } diff --git a/src/main/java/frc/robot/subsystems/drive/DriveConstants.java b/src/main/java/frc/robot/subsystems/drive/DriveConstants.java index 85e34d9..7aff2fa 100644 --- a/src/main/java/frc/robot/subsystems/drive/DriveConstants.java +++ b/src/main/java/frc/robot/subsystems/drive/DriveConstants.java @@ -60,7 +60,10 @@ public PathConstraints getPathConstraints(double speedMultiplier) { new Translation2d(Units.inchesToMeters(22.729228), Units.inchesToMeters(22.729228)), new Translation2d(Units.inchesToMeters(35), Units.inchesToMeters(35)), 7.05968, - 14.5); + 14.5 + * (Constants.isDemoMode() + ? Constants.DEMO_SPEED_TRANSLATION_SPEED_COFICIENT + : 1.0)); case T_SHIRT_CANNON_CHASSIS -> new DriveConfig( new Translation2d(Units.inchesToMeters(22.729226), Units.inchesToMeters(22.729226)), new Translation2d(Units.inchesToMeters(25.729226), Units.inchesToMeters(25.729226)), diff --git a/src/main/java/frc/robot/subsystems/hang/Hang.java b/src/main/java/frc/robot/subsystems/hang/Hang.java index 2094f98..f84250c 100644 --- a/src/main/java/frc/robot/subsystems/hang/Hang.java +++ b/src/main/java/frc/robot/subsystems/hang/Hang.java @@ -38,7 +38,6 @@ public class Hang extends SubsystemBase { public Hang(HangIO io) { this.io = io; io.setBrakeMode(true); - io.setLimits(HangConstants.RELATIVE_MIN_ROTATIONS, HangConstants.RELATIVE_MAX_ROTATIONS); } @Override @@ -60,7 +59,7 @@ public void periodic() { measuredVisualizer.update(Rotation2d.fromRotations(inputs.positionRotations)); } - private void setGoal(Rotation2d rotation) { + public void setGoal(Rotation2d rotation) { runClosedLoop = true; controller.setSetpoint(rotation.getRotations()); setpointVisualizer.update(rotation); @@ -89,18 +88,6 @@ public double getRelativeRotations() { return inputs.positionRotations; } - public Command stow() { - return runOnce(() -> setGoal(HangConstants.STOWED_POSITION_ROTATIONS)); - } - - public Command deploy() { - return runOnce(() -> setGoal(HangConstants.DEPLOY_POSITION_ROTATIONS)); - } - - public Command retract() { - return runOnce(() -> setGoal(HangConstants.RETRACT_POSITION_ROTATIONS)); - } - public Command runSet(double speed) { return runEnd(() -> io.runOpenLoop(speed), io::stop); } diff --git a/src/main/java/frc/robot/subsystems/hang/HangConstants.java b/src/main/java/frc/robot/subsystems/hang/HangConstants.java index 510eff6..c03324d 100644 --- a/src/main/java/frc/robot/subsystems/hang/HangConstants.java +++ b/src/main/java/frc/robot/subsystems/hang/HangConstants.java @@ -1,6 +1,5 @@ package frc.robot.subsystems.hang; -import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.util.Units; import frc.robot.Constants; import frc.robot.utility.records.PIDConstants; @@ -27,12 +26,5 @@ public static record HangConfig( public static final int MOTOR_CURRENT_LIMIT = 40; public static final double GEAR_REDUCTION = 5 * 5 * 5; - public static final Rotation2d STOWED_POSITION_ROTATIONS = Rotation2d.fromDegrees(0); - public static final Rotation2d DEPLOY_POSITION_ROTATIONS = Rotation2d.fromDegrees(60); - public static final Rotation2d RETRACT_POSITION_ROTATIONS = Rotation2d.fromDegrees(-30); - - public static final double RELATIVE_MIN_ROTATIONS = -1; - public static final double RELATIVE_MAX_ROTATIONS = +1; - public static final double TOLERANCE = Units.degreesToRotations(0.3); } diff --git a/src/main/java/frc/robot/subsystems/led/BlinkenLEDPattern.java b/src/main/java/frc/robot/subsystems/led/BlinkenLEDPattern.java new file mode 100644 index 0000000..eac75fe --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/BlinkenLEDPattern.java @@ -0,0 +1,161 @@ +package frc.robot.subsystems.led; + +/** + * @brief LED patterns and solid colors for REV Blinkin + * @link https://www.revrobotics.com/content/docs/REV-11-1105-UM.pdf + */ +public enum BlinkenLEDPattern { + // Off + OFF(1995), // same as SolidColors.BLACK + + // Fixed Palette - Rainbow + RAINBOW_PALETTE(1005), + PARTY_PALETTE(1015), + OCEAN_PALETTE(1025), + LAVA_PALETTE(1035), + FOREST_PALETTE(1045), + + // Fixed Palette - Rainbow with Glitter + RAINBOW_WITH_GLITTER(1055), + + // Fixed Palette - Confetti + CONFETTI(1065), + + // Fixed Palette - Shot + SHOT_RED(1075), + SHOT_BLUE(1085), + SHOT_WHITE(1095), + + // Fixed Palette - Sinelon + SINELON_RAINBOW(1105), + SINELON_PARTY(1115), + SINELON_OCEAN(1125), + SINELON_LAVA(1135), + SINELON_FOREST(1145), + + // Fixed Palette - Beats per Minute + BPM_RAINBOW(1155), + BPM_PARTY(1165), + BPM_OCEAN(1175), + BPM_LAVA(1185), + BPM_FOREST(1195), + + // Fixed Palette - Fire + FIRE_MEDIUM(1205), + FIRE_LARGE(1215), + + // Fixed Palette - Twinkles + TWINKLES_RAINBOW(1225), + TWINKLES_PARTY(1235), + TWINKLES_OCEAN(1245), + TWINKLES_LAVA(1255), + TWINKLES_FOREST(1265), + + // Fixed Palette - Color Waves + COLORWAVES_RAINBOW(1275), + COLORWAVES_PARTY(1285), + COLORWAVES_OCEAN(1295), + COLORWAVES_LAVA(1305), + COLORWAVES_FOREST(1315), + + // Fixed Palette - Larson Scanner + LARSON_RED(1325), + LARSON_GRAY(1335), + + // Fixed Palette - Light Chase + CHASE_RED(1345), + CHASE_BLUE(1355), + CHASE_GRAY(1365), + + // Fixed Palette - Heartbeat + HEARTBEAT_RED(1375), + HEARTBEAT_BLUE(1385), + HEARTBEAT_WHITE(1395), + HEARTBEAT_GRAY(1405), + + // Fixed Palette - Breath + BREATH_RED(1415), + BREATH_BLUE(1425), + BREATH_GRAY(1435), + + // Fixed Palette - Strobe + STROBE_RED(1445), + STROBE_BLUE(1455), + STROBE_GOLD(1465), + STROBE_WHITE(1475), + + // Color 1 Pattern + C1_BLEND_TO_BLACK(1485), + C1_LARSON(1495), + C1_CHASE(1505), + C1_HEARTBEAT_SLOW(1515), + C1_HEARTBEAT_MEDIUM(1525), + C1_HEARTBEAT_FAST(1535), + C1_BREATH_SLOW(1545), + C1_BREATH_FAST(1555), + C1_SHOT(1565), + C1_STROBE(1575), + + // Color 2 Pattern + C2_BLEND_TO_BLACK(1585), + C2_LARSON(1595), + C2_CHASE(1605), + C2_HEARTBEAT_SLOW(1615), + C2_HEARTBEAT_MEDIUM(1625), + C2_HEARTBEAT_FAST(1635), + C2_BREATH_SLOW(1645), + C2_BREATH_FAST(1655), + C2_SHOT(1665), + C2_STROBE(1675), + + // Color 1 & 2 Pattern + SPARKLE_1_ON_2(1685), + SPARKLE_2_ON_1(1695), + COLOR_GRADIENT_1_AND_2(1705), + COLOR_BPM_1_AND_2(1715), + BLEND_1_TO_2(1725), + BLEND_1_AND_2(1735), + COLOR_NO_BLEND_1_AND_2(1745), + COLOR_TWINKLES_1_AND_2(1755), + COLOR_WAVES_1_AND_2(1765), + COLOR_SINELON_1_AND_2(1775), + + // Solid Colors + HOT_PINK(1785), + DARK_RED(1795), + RED(1805), + RED_ORANGE(1815), + ORANGE(1825), + GOLD(1835), + YELLOW(1845), + LAWN_GREEN(1855), + LIME(1865), + DARK_GREEN(1875), + GREEN(1885), + BLUE_GREEN(1895), + AQUA(1905), + SKY_BLUE(1915), + DARK_BLUE(1925), + BLUE(1935), + BLUE_VIOLET(1945), + VIOLET(1955), + WHITE(1965), + GRAY(1975), + DARK_GRAY(1985), + BLACK(1995); + + private final int pulse; + + BlinkenLEDPattern(int pulse) { + this.pulse = pulse; + } + + public int getPulse() { + return pulse; + } + + @Override + public String toString() { + return name() + " (" + pulse + "us)"; + } +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDConstants.java b/src/main/java/frc/robot/subsystems/led/LEDConstants.java index 9f91383..7035526 100644 --- a/src/main/java/frc/robot/subsystems/led/LEDConstants.java +++ b/src/main/java/frc/robot/subsystems/led/LEDConstants.java @@ -1,175 +1,22 @@ package frc.robot.subsystems.led; -import frc.robot.Constants; - -/** - * @brief Patterns taken from - * @link https://www.revrobotics.com/content/docs/REV-11-1105-UM.pdf - */ public class LEDConstants { - public static final int DEFAULT = SolidColors.BLACK; - - public static final int[] PWM_PORTS = - switch (Constants.getRobot()) { - case COMP_BOT_2025 -> new int[] {0, 1}; - case T_SHIRT_CANNON_CHASSIS -> new int[] {0}; - case WOOD_BOT_TWO_2025 -> new int[] {0}; - default -> new int[] {}; - }; - - public static final class FixedPalettePattern { - public static final class Rainbow { - public static final int RAINBOW_PALETTE = 1005; - public static final int PARTY_PALETTE = 1015; - public static final int OCEAN_PALETTE = 1025; - public static final int LAVA_PALETTE = 1035; - public static final int FOREST_PALETTE = 1045; - } - - public static final class RainbowWithGlitter { - public static final int RAINBOW_WITH_GLITTER = 1055; - } - - public static final class Confetti { - public static final int CONFETTI = 1065; - } - public static final class Shot { - public static final int RED = 1075; - public static final int BLUE = 1085; - public static final int WHITE = 1095; - } - - public static final class Sinelon { - public static final int RAINBOW_PALETTE = 1105; - public static final int PARTY_PALETTE = 1115; - public static final int OCEAN_PALETTE = 1125; - public static final int LAVA_PALETTE = 1135; - public static final int FOREST_PALETTE = 1145; - } - - public static final class BeatsPerMinute { - public static final int RAINBOW_PALETTE = 1155; - public static final int PARTY_PALETTE = 1165; - public static final int OCEAN_PALETTE = 1175; - public static final int LAVA_PALETTE = 1185; - public static final int FOREST_PALETTE = 1195; - } + public static final BlinkenLEDPattern DEFAULT_PATTERN = BlinkenLEDPattern.OFF; - public static final class Fire { - public static final int MEDIUM = 1205; - public static final int LARGE = 1215; - } + public enum BlinkenMode { + STRIP_5V(2125), + STRIP_12V(2145); - public static final class Twinkles { - public static final int RAINBOW_PALETTE = 1225; - public static final int PARTY_PALETTE = 1235; - public static final int OCEAN_PALETTE = 1245; - public static final int LAVA_PALETTE = 1255; - public static final int FOREST_PALETTE = 1265; - } + public final int setupPulse; - public static final class ColorWaves { - public static final int RAINBOW_PALETTE = 1275; - public static final int PARTY_PALETTE = 1285; - public static final int OCEAN_PALETTE = 1295; - public static final int LAVA_PALETTE = 1305; - public static final int FOREST_PALETTE = 1315; + BlinkenMode(int setupPulse) { + this.setupPulse = setupPulse; } - - public static final class LarsonScanner { - public static final int RED = 1325; - public static final int GRAY = 1335; - } - - public static final class LightChase { - public static final int RED = 1345; - public static final int BLUE = 1355; - public static final int GRAY = 1365; - } - - public static final class Heartbeat { - public static final int RED = 1375; - public static final int BLUE = 1385; - public static final int WHITE = 1395; - public static final int GRAY = 1405; - } - - public static final class Breath { - public static final int RED = 1415; - public static final int BLUE = 1425; - public static final int GRAY = 1435; - } - - public static final class Strobe { - public static final int RED = 1445; - public static final int BLUE = 1455; - public static final int GOLD = 1465; - public static final int WHITE = 1475; - } - } - - public static final class Color1Pattern { - public static final int END_TO_END_BLEND_TO_BLACK = 1485; - public static final int LARSON_SCANNER = 1495; - public static final int LIGHT_CHASE = 1505; - public static final int HEARTBEAT_SLOW = 1515; - public static final int HEARTBEAT_MEDIUM = 1525; - public static final int HEARTBEAT_FAST = 1535; - public static final int BREATH_SLOW = 1545; - public static final int BREATH_FAST = 1555; - public static final int SHOT = 1565; - public static final int STROBE = 1575; } - public static final class Color2Pattern { - public static final int END_TO_END_BLEND_TO_BLACK = 1585; - public static final int LARSON_SCANNER = 1595; - public static final int LIGHT_CHASE = 1605; - public static final int HEARTBEAT_SLOW = 1615; - public static final int HEARTBEAT_MEDIUM = 1625; - public static final int HEARTBEAT_FAST = 1635; - public static final int BREATH_SLOW = 1645; - public static final int BREATH_FAST = 1655; - public static final int SHOT = 1665; - public static final int STROBE = 1675; - } + public record LEDConfig(int pwmChannel, BlinkenMode mode) {} - public static final class Color1And2Pattern { - public static final int SPARKLE_1_ON_2 = 1685; - public static final int SPARKLE_2_ON_1 = 1695; - public static final int COLOR_GRADIENT = 1705; - public static final int BEATS_PER_MINUTE = 1715; - public static final int END_TO_END_BLEND_1_TO_2 = 1725; - public static final int END_TO_END_BLEND = 1735; - public static final int COLOR_1_AND_2_NO_BLENDING = 1745; - public static final int TWINKLES = 1755; - public static final int COLOR_WAVES = 1765; - public static final int SINELON = 1775; - } - - public static final class SolidColors { - public static final int HOT_PINK = 1785; - public static final int DARK_RED = 1795; - public static final int RED = 1805; - public static final int RED_ORANGE = 1815; - public static final int ORANGE = 1825; - public static final int GOLD = 1835; - public static final int YELLOW = 1845; - public static final int LAWN_GREEN = 1855; - public static final int LIME = 1865; - public static final int DARK_GREEN = 1875; - public static final int GREEN = 1885; - public static final int BLUE_GREEN = 1895; - public static final int AQUA = 1905; - public static final int SKY_BLUE = 1915; - public static final int DARK_BLUE = 1925; - public static final int BLUE = 1935; - public static final int BLUE_VIOLET = 1945; - public static final int VIOLET = 1955; - public static final int WHITE = 1965; - public static final int GRAY = 1975; - public static final int DARK_GRAY = 1985; - public static final int BLACK = 1995; - } + public static final LEDConfig LEDS_STRIP_2025_LEFT = new LEDConfig(0, BlinkenMode.STRIP_5V); + public static final LEDConfig LEDS_STRIP_2025_RIGHT = new LEDConfig(1, BlinkenMode.STRIP_5V); } diff --git a/src/main/java/frc/robot/subsystems/led/LEDStripIO.java b/src/main/java/frc/robot/subsystems/led/LEDStripIO.java new file mode 100644 index 0000000..c1d5c49 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDStripIO.java @@ -0,0 +1,22 @@ +package frc.robot.subsystems.led; + +import org.littletonrobotics.junction.AutoLog; + +/** IO layer interface for april tag detection systems */ +public interface LEDStripIO { + @AutoLog + public static class LEDStripIOInputs { + int targetPulse; + int measuredPulse; + boolean runningSetup = false; + + BlinkenLEDPattern requestedPattern; + } + + /** Updates the set of loggable inputs. */ + default void updateInputs(LEDStripIOInputs inputs) {} + + public default void setPattern(BlinkenLEDPattern pattern) {} + + public default void runSetup(boolean run) {} +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDStripIOBlinken.java b/src/main/java/frc/robot/subsystems/led/LEDStripIOBlinken.java new file mode 100644 index 0000000..60d5537 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDStripIOBlinken.java @@ -0,0 +1,45 @@ +package frc.robot.subsystems.led; + +import edu.wpi.first.wpilibj.PWM; +import frc.robot.subsystems.led.LEDConstants.BlinkenMode; +import frc.robot.subsystems.led.LEDConstants.LEDConfig; + +public class LEDStripIOBlinken implements LEDStripIO { + + private final PWM pwm; + private final BlinkenMode mode; + + private BlinkenLEDPattern pattern; + private boolean runSetup; + + public LEDStripIOBlinken(LEDConfig config, BlinkenLEDPattern initialPattern) { + pwm = new PWM(config.pwmChannel()); + mode = config.mode(); + pattern = initialPattern; + } + + @Override + public void updateInputs(LEDStripIOInputs inputs) { + inputs.runningSetup = runSetup; + inputs.requestedPattern = pattern; + + inputs.targetPulse = inputs.requestedPattern.getPulse(); + if (runSetup) { + inputs.targetPulse = mode.setupPulse; + } + + pwm.setPulseTimeMicroseconds(inputs.targetPulse); + + inputs.measuredPulse = pwm.getPulseTimeMicroseconds(); + } + + @Override + public void runSetup(boolean run) { + runSetup = run; + } + + @Override + public void setPattern(BlinkenLEDPattern pattern) { + this.pattern = pattern; + } +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDStripIOSim.java b/src/main/java/frc/robot/subsystems/led/LEDStripIOSim.java new file mode 100644 index 0000000..fda25e8 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/led/LEDStripIOSim.java @@ -0,0 +1,34 @@ +package frc.robot.subsystems.led; + +public class LEDStripIOSim implements LEDStripIO { + + private BlinkenLEDPattern pattern; + private boolean runSetup; + + public LEDStripIOSim(BlinkenLEDPattern initialPattern) { + pattern = initialPattern; + } + + @Override + public void updateInputs(LEDStripIOInputs inputs) { + inputs.runningSetup = runSetup; + inputs.requestedPattern = pattern; + + inputs.targetPulse = inputs.requestedPattern.getPulse(); + if (runSetup) { + inputs.targetPulse = -1; + } + + inputs.measuredPulse = inputs.targetPulse; + } + + @Override + public void runSetup(boolean run) { + runSetup = run; + } + + @Override + public void setPattern(BlinkenLEDPattern pattern) { + this.pattern = pattern; + } +} diff --git a/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java b/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java index aa20629..e1f898f 100644 --- a/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java +++ b/src/main/java/frc/robot/subsystems/led/LEDSubsystem.java @@ -1,128 +1,75 @@ package frc.robot.subsystems.led; +import edu.wpi.first.math.filter.Debouncer; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.DriverStation.Alliance; -import edu.wpi.first.wpilibj.PWM; -import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.SubsystemBase; -import frc.robot.subsystems.led.LEDConstants.FixedPalettePattern; -import frc.robot.subsystems.led.LEDConstants.SolidColors; -import java.util.Optional; +import java.util.Arrays; +import java.util.function.Supplier; import java.util.stream.Stream; -import org.littletonrobotics.junction.AutoLogOutput; import org.littletonrobotics.junction.Logger; +/** + * Subsystem to manage multiple LED strips with Blinken LED Drivers. + * + *

LEDs will retain their last set pattern until a new pattern is set, unless a default pattern + * is given though a default command. + */ public class LEDSubsystem extends SubsystemBase { - private final Timer startupTimer = new Timer(); + private LEDStripIO[] strips; + private LEDStripIOInputsAutoLogged[] inputs; - // In future, this could be an IO layer - private class LEDStrip { - private final PWM pwm; - private int pulse; + private final Debouncer setupDebouncer = new Debouncer(0.8); - public LEDStrip(PWM pwmController, int initialPattern) { - pwm = pwmController; - pulse = initialPattern; - } - - public void setPulse(int pulse) { - this.pulse = pulse; - } - - public void update() { - pwm.setPulseTimeMicroseconds(pulse); - } - } + public LEDSubsystem(LEDStripIO... strips) { + this.strips = strips; - private LEDStrip[] strips; + inputs = + Arrays.stream(strips) + .map(s -> new LEDStripIOInputsAutoLogged()) + .toArray(LEDStripIOInputsAutoLogged[]::new); - public LEDSubsystem(int... pwmPorts) { - - // Create all the strip objects - strips = new LEDStrip[pwmPorts.length]; - for (int i = 0; i < pwmPorts.length; i++) { - strips[i] = new LEDStrip(new PWM(pwmPorts[i]), LEDConstants.DEFAULT); - } + Logger.recordOutput("led/numStrips", strips.length); } - private Optional alliance = Optional.empty(); - @Override public void periodic() { + boolean runSetup = + !setupDebouncer.calculate(DriverStation.isEnabled()) && DriverStation.isEnabled(); - if (DriverStation.isEnabled() && !startupTimer.isRunning()) { - startupTimer.restart(); - } else if (startupTimer.hasElapsed(0.08)) { - startupTimer.stop(); - } - - if (startupTimer.isRunning()) { - applyAll(2125); // 5 volt mode - } else { - alliance = DriverStation.getAlliance(); - - if (DriverStation.isEStopped()) { - applyAll(SolidColors.GRAY); - } else if (DriverStation.isDisabled()) { - applyAll(SolidColors.BLUE, SolidColors.RED, SolidColors.WHITE); - } else { - applyAll( - FixedPalettePattern.ColorWaves.OCEAN_PALETTE, - FixedPalettePattern.ColorWaves.LAVA_PALETTE, - FixedPalettePattern.ColorWaves.FOREST_PALETTE); - } + for (int i = 0; i < strips.length; i++) { + strips[i].runSetup(runSetup); + strips[i].updateInputs(inputs[i]); + Logger.processInputs("LED/strip" + i, inputs[i]); } - - // Update all the strips - strips().forEach(LEDStrip::update); - - Logger.recordOutput("LED/pulses", strips().mapToInt(strip -> strip.pulse).toArray()); } - /** - * Apply a pattern to a specific LED strip - * - * @param strip The strip index to apply to. These are the same as the indices of LEDStrip objects - * in the constructor - * @param pattern The pattern to apply to the strip. See the below link for details, or use the - * LEDPatterns constant. - * @link https://www.revrobotics.com/content/docs/REV-11-1105-UM.pdf - */ - public void apply(int strip, int pattern) { - if (strip >= 0 && strip < strips.length) strips[strip].setPulse(pattern); + public Command applyColor(BlinkenLEDPattern color) { + return run(() -> set(color)); } - /** - * Apply a pattern to all LED strips - * - * @param pattern The pattern to apply to the strips. See the below link for details, or use the - * LEDPatterns constant. - * @link https://www.revrobotics.com/content/docs/REV-11-1105-UM.pdf - */ - public void applyAll(int pattern) { - for (LEDStrip strip : strips) { - strip.setPulse(pattern); - } + public Command applyColor(Supplier color) { + return run(() -> set(color.get())); } - @AutoLogOutput(key = "LED/hasSetUp") - public boolean hasSetUp() { - return !startupTimer.isRunning(); + public Command applyColor( + BlinkenLEDPattern colorIfBlue, + BlinkenLEDPattern colorIfRed, + BlinkenLEDPattern colorIfUnknown) { + return applyColor( + () -> + DriverStation.getAlliance() + .map(a -> a == Alliance.Blue ? colorIfBlue : colorIfRed) + .orElse(colorIfUnknown)); } - /** - * Apply a pattern to all LED strips based on the alliance color - * - * @param blue The pattern to apply if the alliance color is blue - * @param red The pattern to apply if the alliance color is red - * @param backup The pattern to apply if the alliance color is unknown - */ - private void applyAll(int blue, int red, int backup) { - applyAll(alliance.map(a -> a == Alliance.Blue ? blue : red).orElse(backup)); + public Command turnOff() { + return applyColor(BlinkenLEDPattern.OFF); } - private Stream strips() { - return Stream.of(strips); + public void set(BlinkenLEDPattern pattern) { + Stream.of(strips).forEach(strip -> strip.setPattern(pattern)); } } diff --git a/src/main/java/frc/robot/subsystems/superstructure/wrist/Wrist.java b/src/main/java/frc/robot/subsystems/superstructure/wrist/Wrist.java index e90d843..cde9f80 100644 --- a/src/main/java/frc/robot/subsystems/superstructure/wrist/Wrist.java +++ b/src/main/java/frc/robot/subsystems/superstructure/wrist/Wrist.java @@ -56,19 +56,28 @@ public class Wrist extends SubsystemBase { private final WristIO io; private final WristIOInputsAutoLogged inputs = new WristIOInputsAutoLogged(); - private TrapezoidProfile profile; - private TrapezoidProfile profileSlow; - private BooleanSupplier slowModeSupplier = () -> false; + private final Alert motorConnectedAlert = + new Alert("Wrist motor disconnected!", Alert.AlertType.kError); + + private final Debouncer disabledDebouncer = new Debouncer(3, DebounceType.kRising); + + enum IdleModeControl { + BRAKE, + AUTO, + COAST + } + + private IdleModeControl coastMode = IdleModeControl.BRAKE; + private boolean brakeModeEnabled = true; private ArmFeedforward feedforward; private State setpoint = null; private Supplier goalSupplier = State::new; - private final Alert motorConnectedAlert = - new Alert("Wrist motor disconnected!", Alert.AlertType.kError); - - private final Debouncer disabledDebouncer = new Debouncer(3, DebounceType.kRising); + private TrapezoidProfile profile; + private TrapezoidProfile profileSlow; + private BooleanSupplier slowModeSupplier = () -> false; /** Creates a new Wrist. */ public Wrist(WristIO io) { @@ -79,7 +88,7 @@ public Wrist(WristIO io) { new TrapezoidProfile(new Constraints(maxVelocitySlow.get(), maxAccelerationSlow.get())); io.setPID(kP.get(), kI.get(), kD.get()); - io.setBrakeMode(true); + io.setBrakeMode(brakeModeEnabled); this.feedforward = new ArmFeedforward(kS.get(), kG.get(), kV.get(), kA.get()); } @@ -91,7 +100,45 @@ public void periodic() { Logger.recordOutput("Wrist/slowMode", slowModeSupplier.getAsBoolean()); - io.setBrakeMode(!disabledDebouncer.calculate(DriverStation.isDisabled())); + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> { + io.setPID(values[0], values[1], values[2]); + }, + kP, + kI, + kD); + + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> feedforward = new ArmFeedforward(values[0], values[1], values[2], values[3]), + kS, + kG, + kV, + kA); + + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> profile = new TrapezoidProfile(new Constraints(values[0], values[1])), + maxVelocity, + maxAcceleration); + + LoggedTunableNumber.ifChanged( + hashCode(), + (values) -> profileSlow = new TrapezoidProfile(new Constraints(values[0], values[1])), + maxVelocitySlow, + maxAccelerationSlow); + + brakeModeEnabled = + switch (coastMode) { + case BRAKE -> true; + case AUTO -> !disabledDebouncer.calculate(DriverStation.isDisabled()); + case COAST -> false; + }; + + io.setBrakeMode(brakeModeEnabled); + + Logger.recordOutput("Wrist/breakModeEnabled", brakeModeEnabled); if (DriverStation.isEnabled()) { Logger.recordOutput("Wrist/shouldRunProfiled", true); @@ -128,35 +175,6 @@ public void periodic() { setpoint = new State(inputs.positionRad, inputs.velocityRadPerSec); } - LoggedTunableNumber.ifChanged( - hashCode(), - (values) -> { - io.setPID(values[0], values[1], values[2]); - }, - kP, - kI, - kD); - - LoggedTunableNumber.ifChanged( - hashCode(), - (values) -> feedforward = new ArmFeedforward(values[0], values[1], values[2], values[3]), - kS, - kG, - kV, - kA); - - LoggedTunableNumber.ifChanged( - hashCode(), - (values) -> profile = new TrapezoidProfile(new Constraints(values[0], values[1])), - maxVelocity, - maxAcceleration); - - LoggedTunableNumber.ifChanged( - hashCode(), - (values) -> profileSlow = new TrapezoidProfile(new Constraints(values[0], values[1])), - maxVelocitySlow, - maxAccelerationSlow); - motorConnectedAlert.set(!inputs.motorConnected); } diff --git a/src/main/java/frc/robot/subsystems/superstructure/wrist/WristIOHardware.java b/src/main/java/frc/robot/subsystems/superstructure/wrist/WristIOHardware.java index cc02db2..f6353e5 100644 --- a/src/main/java/frc/robot/subsystems/superstructure/wrist/WristIOHardware.java +++ b/src/main/java/frc/robot/subsystems/superstructure/wrist/WristIOHardware.java @@ -22,6 +22,10 @@ import frc.robot.utility.SparkUtil; public class WristIOHardware implements WristIO { + + private static final FeedbackSensor SENSOR_TYPE = FeedbackSensor.kAbsoluteEncoder; + // private final static FeedbackSensor SENSOR_TYPE = FeedbackSensor.kPrimaryEncoder; + private final SparkMax motor; private final RelativeEncoder encoder; private final SparkAbsoluteEncoder absEncoder; @@ -52,7 +56,7 @@ public WristIOHardware(WristConfig config) { .inverted(config.encoderInverted()) .zeroOffset(config.absoluteEncoderOffset()) .zeroCentered(true); - motorConfig.closedLoop.pidf(0, 0, 0, 0).feedbackSensor(FeedbackSensor.kAbsoluteEncoder); + motorConfig.closedLoop.pidf(0, 0, 0, 0).feedbackSensor(SENSOR_TYPE); motor.configure(motorConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); diff --git a/src/main/java/frc/robot/subsystems/vision/AprilTagVision.java b/src/main/java/frc/robot/subsystems/vision/AprilTagVision.java index 05f86d4..79a04fd 100644 --- a/src/main/java/frc/robot/subsystems/vision/AprilTagVision.java +++ b/src/main/java/frc/robot/subsystems/vision/AprilTagVision.java @@ -1,5 +1,6 @@ package frc.robot.subsystems.vision; +import edu.wpi.first.apriltag.AprilTagFieldLayout; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.filter.Debouncer; import edu.wpi.first.math.filter.Debouncer.DebounceType; @@ -8,6 +9,8 @@ import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.subsystems.vision.Camera.AbsoluteTrackedTarget; +import frc.robot.subsystems.vision.Camera.RelativeTrackedTarget; import frc.robot.subsystems.vision.Camera.VisionResult; import frc.robot.subsystems.vision.Camera.VisionResultStatus; import java.util.ArrayList; @@ -19,10 +22,6 @@ import org.littletonrobotics.junction.Logger; public class AprilTagVision extends SubsystemBase { - - public static final boolean DO_SUMMARY_LOGGING = true; - public static final boolean DO_CAMERA_LOGGING = true; - private final Camera[] cameras; private final Debouncer debouncer = new Debouncer(0.2, DebounceType.kFalling); @@ -32,6 +31,9 @@ public class AprilTagVision extends SubsystemBase { private boolean hasVisionEstimate = false; + private AprilTagFieldLayout fieldTags = null; + private List simulatedTargets = new ArrayList<>(); + public AprilTagVision(CameraIO... camerasIO) { this.cameras = Arrays.stream(camerasIO).map(io -> new Camera(io)).toArray(Camera[]::new); } @@ -39,6 +41,14 @@ public AprilTagVision(CameraIO... camerasIO) { @Override public void periodic() { + AprilTagFieldLayout fieldTags = this.fieldTags; + for (SimControlledTarget target : simulatedTargets) { + fieldTags = target.createFieldWithTarget(fieldTags); + } + for (Camera camera : cameras) { + camera.setFieldTags(fieldTags); + } + // Run periodic for all cameras, as they are not real subsystems for (Camera camera : cameras) { camera.periodic(); @@ -56,21 +66,15 @@ public void periodic() { for (Camera camera : cameras) { String cameraRoot = root + "/" + camera.getCameraName(); - if (DO_CAMERA_LOGGING && camera.getResults().length == 0) { - Logger.recordOutput(cameraRoot + "/tagsUsedPositions", new Pose3d[] {}); - Logger.recordOutput(cameraRoot + "/positionEstimate", new Pose3d[] {}); - Logger.recordOutput(cameraRoot + "/status", VisionResultStatus.NO_DATA); - Logger.recordOutput(cameraRoot + "/success", false); - } + Logger.recordOutput(cameraRoot + "/tagsUsedPositions", new Pose3d[] {}); + Logger.recordOutput(cameraRoot + "/positionEstimate", new Pose3d[] {}); + Logger.recordOutput(cameraRoot + "/status", VisionResultStatus.NO_DATA); + Logger.recordOutput(cameraRoot + "/success", false); // Loop through all results that the camera has for (VisionResult result : camera.getResults()) { if (!result.hasNewData()) { - Logger.recordOutput(cameraRoot + "/tagsUsedPositions", new Pose3d[] {}); - Logger.recordOutput(cameraRoot + "/positionEstimate", new Pose3d[] {}); - Logger.recordOutput(cameraRoot + "/status", VisionResultStatus.NO_DATA); - Logger.recordOutput(cameraRoot + "/success", false); continue; } @@ -84,24 +88,17 @@ public void periodic() { hasVisionEstimate = hasVisionEstimate || visionEstimate.isSuccess(); - // Logging - if (DO_CAMERA_LOGGING) { - Logger.recordOutput(cameraRoot + "/tagsUsedPositions", result.tagPositionsOnField()); - - Logger.recordOutput(cameraRoot + "/positionEstimate", visionEstimate.robotPose()); - - Logger.recordOutput(cameraRoot + "/status", visionEstimate.status()); - Logger.recordOutput(cameraRoot + "/success", visionEstimate.status().isSuccess()); - } + Logger.recordOutput(cameraRoot + "/tagsUsedPositions", result.tagPositionsOnField()); + Logger.recordOutput(cameraRoot + "/positionEstimate", visionEstimate.robotPose()); + Logger.recordOutput(cameraRoot + "/status", visionEstimate.status()); + Logger.recordOutput(cameraRoot + "/success", visionEstimate.status().isSuccess()); - if (DO_SUMMARY_LOGGING) { - if (visionEstimate.isSuccess()) { - robotPosesAccepted.add(visionEstimate.robotPose()); - } else { - robotPosesRejected.add(visionEstimate.robotPose()); - } - tagPoses.addAll(Arrays.asList(result.tagPositionsOnField())); + if (visionEstimate.isSuccess()) { + robotPosesAccepted.add(visionEstimate.robotPose()); + } else { + robotPosesRejected.add(visionEstimate.robotPose()); } + tagPoses.addAll(Arrays.asList(result.tagPositionsOnField())); for (Consumer consumer : timestampRobotPoseEstimateConsumers) { @@ -110,11 +107,25 @@ public void periodic() { } } - if (DO_SUMMARY_LOGGING) { - Logger.recordOutput(root + "/robotPosesAccepted", robotPosesAccepted.toArray(Pose3d[]::new)); - Logger.recordOutput(root + "/robotPosesRejected", robotPosesRejected.toArray(Pose3d[]::new)); - Logger.recordOutput(root + "/tagPoses", tagPoses.toArray(Pose3d[]::new)); - } + Logger.recordOutput(root + "/robotPosesAccepted", robotPosesAccepted.toArray(Pose3d[]::new)); + Logger.recordOutput(root + "/robotPosesRejected", robotPosesRejected.toArray(Pose3d[]::new)); + Logger.recordOutput(root + "/tagPoses", tagPoses.toArray(Pose3d[]::new)); + } + + public List getRelativeTrackedTargets() { + return Arrays.stream(cameras).flatMap(camera -> camera.getRelativeTargets().stream()).toList(); + } + + public List getAbsoluteTrackedTargets() { + return Arrays.stream(cameras).flatMap(camera -> camera.getAbsoluteTargets().stream()).toList(); + } + + public void setFieldTags(AprilTagFieldLayout fieldTags) { + this.fieldTags = fieldTags; + } + + public void addSimulatedTarget(SimControlledTarget target) { + simulatedTargets.add(target); } /** Get whether or not the vision system has a valid estimate */ @@ -122,18 +133,19 @@ public boolean hasVisionEstimate() { return hasVisionEstimate; } - public boolean hasVisionEstimateDebounce() { + public boolean hasStableVisionEstimate() { return debouncer.calculate(hasVisionEstimate); } /** * Set the last robot pose supplier for all cameras * + * @param filter if robot pose filtering should be applied * @param lastRobotPose the last robot pose supplier */ - public void setLastRobotPoseSupplier(Supplier lastRobotPose) { + public void filterBasedOnLastPose(boolean filter, Supplier lastRobotPose) { for (Camera camera : cameras) { - camera.setLastRobotPoseSupplier(lastRobotPose); + camera.filterBasedOnLastPose(filter, lastRobotPose); } } diff --git a/src/main/java/frc/robot/subsystems/vision/Camera.java b/src/main/java/frc/robot/subsystems/vision/Camera.java index e544bb3..28b4c5c 100644 --- a/src/main/java/frc/robot/subsystems/vision/Camera.java +++ b/src/main/java/frc/robot/subsystems/vision/Camera.java @@ -1,18 +1,22 @@ package frc.robot.subsystems.vision; +import edu.wpi.first.apriltag.AprilTagFieldLayout; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.VecBuilder; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.math.util.Units; import edu.wpi.first.wpilibj.Alert; +import frc.robot.subsystems.vision.VisionConstants.CameraConfig; import frc.robot.utility.tunable.LoggedTunableNumber; import frc.robot.utility.tunable.LoggedTunableNumberFactory; import java.util.Arrays; import java.util.DoubleSummaryStatistics; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Supplier; @@ -43,6 +47,7 @@ public class Camera { private final CameraIO io; private final CameraIOInputsAutoLogged inputs = new CameraIOInputsAutoLogged(); + private AprilTagFieldLayout aprilTagFieldLayout; private Set tagsIdsOnField; private VisionResult[] results = new VisionResult[0]; @@ -60,6 +65,31 @@ public static record VisionResult( Matrix standardDeviation, VisionResultStatus status) {} + public record RelativeTrackedTarget( + int id, Transform3d cameraToTarget, CameraConfig camera, double poseAmbiguity) {} + + public record AbsoluteTrackedTarget( + int id, + Pose3d targetPose, + Pose3d cameraPose, + Transform3d cameraToTarget, + CameraConfig camera, + double poseAmbiguity, + Pose2d robotPose) { + public AbsoluteTrackedTarget(RelativeTrackedTarget relativeTarget, Pose2d robotPose) { + this( + relativeTarget.id, + new Pose3d(robotPose) + .plus(relativeTarget.camera.robotToCamera()) + .plus(relativeTarget.cameraToTarget), + new Pose3d(robotPose).plus(relativeTarget.camera.robotToCamera()), + relativeTarget.cameraToTarget, + relativeTarget.camera, + relativeTarget.poseAmbiguity, + robotPose); + } + } + /** * Create a new robot camera with IO layer * @@ -68,17 +98,21 @@ public static record VisionResult( public Camera(CameraIO io) { this.io = io; - io.setAprilTagFieldLayout(VisionConstants.FIELD); - this.tagsIdsOnField = - VisionConstants.FIELD.getTags().stream().map((tag) -> tag.ID).collect(Collectors.toSet()); - this.missingCameraAlert = new Alert(String.format("Missing cameras %s", getCameraName()), Alert.AlertType.kWarning); } + public void setFieldTags(AprilTagFieldLayout field) { + if (field != this.aprilTagFieldLayout) { + io.setAprilTagFieldLayout(field); + tagsIdsOnField = field.getTags().stream().map((tag) -> tag.ID).collect(Collectors.toSet()); + this.aprilTagFieldLayout = field; + } + } + /** Get name of camera as specified by IO */ public String getCameraName() { - return io.getCameraPosition() + " (" + io.getCameraName() + ")"; + return io.getCameraConfig() + " (" + io.getCameraName() + ")"; } /** Run periodic of module. Updates the set of loggable inputs, updating vision result. */ @@ -92,6 +126,13 @@ public void periodic() { Pose3d[] tagPositionsOnField = getTagPositionsOnField(inputs.tagsUsed[i]); if (inputs.hasNewData[i]) { + Matrix standardDeviations = + getStandardDeviations(tagPositionsOnField, inputs.estimatedRobotPose[i]); + VisionResultStatus status = + lastRobotPoseSupplier == null + ? getStatus(inputs.estimatedRobotPose[i], inputs.tagsUsed[i]) + : getStatus( + inputs.estimatedRobotPose[i], inputs.tagsUsed[i], lastRobotPoseSupplier.get()); results[i] = new VisionResult( true, @@ -99,13 +140,8 @@ public void periodic() { inputs.timestampSecondFPGA[i], inputs.tagsUsed[i], tagPositionsOnField, - getStandardDeviations(tagPositionsOnField, inputs.estimatedRobotPose[i]), - lastRobotPoseSupplier == null - ? getStatus(inputs.estimatedRobotPose[i], inputs.tagsUsed[i]) - : getStatus( - inputs.estimatedRobotPose[i], - inputs.tagsUsed[i], - lastRobotPoseSupplier.get())); + standardDeviations, + status); } else { results[i] = new VisionResult( @@ -120,8 +156,17 @@ public void periodic() { } } - public void setLastRobotPoseSupplier(Supplier lastRobotPose) { - this.lastRobotPoseSupplier = lastRobotPose; + public List getAbsoluteTargets() { + return io.getAbsoluteTargets(); + } + + public List getRelativeTargets() { + return io.getRelativeTargets(); + } + + public void filterBasedOnLastPose(boolean filter, Supplier lastRobotPose) { + this.io.setLastPoseSupplier(lastRobotPose); + this.lastRobotPoseSupplier = filter ? lastRobotPose : null; } public VisionResult[] getResults() { @@ -130,7 +175,7 @@ public VisionResult[] getResults() { private Pose3d[] getTagPositionsOnField(int[] tagsUsed) { return Arrays.stream(tagsUsed) - .mapToObj(VisionConstants.FIELD::getTagPose) + .mapToObj(aprilTagFieldLayout::getTagPose) .filter(Optional::isPresent) .map(Optional::get) .toArray(Pose3d[]::new); @@ -206,8 +251,8 @@ private VisionResultStatus getStatus(Pose3d estimatedRobotPose, int[] tagsUsed) if (estimatedRobotPose.getX() < 0 || estimatedRobotPose.getY() < 0 - || estimatedRobotPose.getX() > VisionConstants.FIELD.getFieldLength() - || estimatedRobotPose.getY() > VisionConstants.FIELD.getFieldWidth()) { + || estimatedRobotPose.getX() > aprilTagFieldLayout.getFieldLength() + || estimatedRobotPose.getY() > aprilTagFieldLayout.getFieldWidth()) { return VisionResultStatus.INVALID_POSE_OUTSIDE_FIELD; } diff --git a/src/main/java/frc/robot/subsystems/vision/CameraIO.java b/src/main/java/frc/robot/subsystems/vision/CameraIO.java index 4afb333..b70ea6e 100644 --- a/src/main/java/frc/robot/subsystems/vision/CameraIO.java +++ b/src/main/java/frc/robot/subsystems/vision/CameraIO.java @@ -1,7 +1,13 @@ package frc.robot.subsystems.vision; import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; +import frc.robot.subsystems.vision.Camera.AbsoluteTrackedTarget; +import frc.robot.subsystems.vision.Camera.RelativeTrackedTarget; +import frc.robot.subsystems.vision.VisionConstants.CameraConfig; +import java.util.List; +import java.util.function.Supplier; import org.littletonrobotics.junction.AutoLog; /** IO layer interface for april tag detection systems */ @@ -21,13 +27,23 @@ public static class CameraIOInputs { /** Updates the set of loggable inputs. */ default void updateInputs(CameraIOInputs inputs) {} + default void setLastPoseSupplier(Supplier lastPoseSupplier) {} + + default List getRelativeTargets() { + return List.of(); + } + + default List getAbsoluteTargets() { + return List.of(); + } + /** Get name of io camera */ default String getCameraName() { return "Camera"; } - default String getCameraPosition() { - return getCameraName(); + default CameraConfig getCameraConfig() { + return null; } /** Set april tag field layout to use */ diff --git a/src/main/java/frc/robot/subsystems/vision/CameraIOPhotonVision.java b/src/main/java/frc/robot/subsystems/vision/CameraIOPhotonVision.java index 005bbd3..c63fe7d 100644 --- a/src/main/java/frc/robot/subsystems/vision/CameraIOPhotonVision.java +++ b/src/main/java/frc/robot/subsystems/vision/CameraIOPhotonVision.java @@ -1,10 +1,15 @@ package frc.robot.subsystems.vision; import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; +import frc.robot.subsystems.vision.Camera.AbsoluteTrackedTarget; +import frc.robot.subsystems.vision.Camera.RelativeTrackedTarget; import frc.robot.subsystems.vision.VisionConstants.CameraConfig; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import org.photonvision.EstimatedRobotPose; import org.photonvision.PhotonCamera; import org.photonvision.PhotonPoseEstimator; @@ -17,10 +22,14 @@ public class CameraIOPhotonVision implements CameraIO { private final PhotonCamera camera; private final PhotonPoseEstimator photonPoseEstimator; - private final String cameraPositionTitle; + private final CameraConfig config; + + private List latestTargets = new ArrayList<>(); + + private Supplier lastPoseSupplier; public CameraIOPhotonVision(CameraConfig config) { - this.cameraPositionTitle = config.cameraPosition(); + this.config = config; // --- Setup Camera --- camera = new PhotonCamera(config.cameraName()); @@ -38,7 +47,7 @@ public CameraIOPhotonVision(CameraConfig config) { photonPoseEstimator = new PhotonPoseEstimator( - VisionConstants.FIELD, + VisionConstants.DEFAULT_FIELD, PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, config.robotToCamera()); @@ -54,8 +63,13 @@ protected PhotonPoseEstimator getPhotonPoseEstimator() { } @Override - public String getCameraPosition() { - return cameraPositionTitle; + public void setLastPoseSupplier(Supplier robotPoseSupplier) { + this.lastPoseSupplier = robotPoseSupplier; + } + + @Override + public CameraConfig getCameraConfig() { + return config; } @Override @@ -75,9 +89,18 @@ public void updateInputs(CameraIOInputs inputs) { inputs.updatesReceived = pipelineResults.size(); + if (lastPoseSupplier != null) { + photonPoseEstimator.setLastPose(lastPoseSupplier.get()); + } + + this.latestTargets.clear(); + for (int i = 0; i < pipelineResults.size(); i++) { + PhotonPipelineResult cameraResult = pipelineResults.get(i); Optional estimatedRobotPoseOptional = - photonPoseEstimator.update(pipelineResults.get(i)); + photonPoseEstimator.update(cameraResult); + + this.latestTargets.addAll(cameraResult.getTargets()); if (estimatedRobotPoseOptional.isPresent()) { @@ -106,6 +129,27 @@ public void updateInputs(CameraIOInputs inputs) { inputs.connected = camera.isConnected(); } + @Override + public List getRelativeTargets() { + return latestTargets.stream() + .map( + t -> + new RelativeTrackedTarget( + t.getFiducialId(), t.getBestCameraToTarget(), config, t.getPoseAmbiguity())) + .toList(); + } + + @Override + public List getAbsoluteTargets() { + if (lastPoseSupplier == null) { + throw new IllegalStateException( + "Robot pose supplier not set for absolute target calculation"); + } + return getRelativeTargets().stream() + .map(r -> new AbsoluteTrackedTarget(r, lastPoseSupplier.get())) + .toList(); + } + @Override public String getCameraName() { return camera.getName(); diff --git a/src/main/java/frc/robot/subsystems/vision/SimControlledTarget.java b/src/main/java/frc/robot/subsystems/vision/SimControlledTarget.java new file mode 100644 index 0000000..4f35f51 --- /dev/null +++ b/src/main/java/frc/robot/subsystems/vision/SimControlledTarget.java @@ -0,0 +1,87 @@ +package frc.robot.subsystems.vision; + +import edu.wpi.first.apriltag.AprilTag; +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.wpilibj.XboxController; +import frc.robot.utility.JoystickUtil; +import frc.robot.utility.VirtualSubsystem; +import java.util.List; +import org.littletonrobotics.junction.Logger; + +public class SimControlledTarget extends VirtualSubsystem { + + private static final double TRANSLATION_SPEED = 0.03; + private static final double TRIGGER_SPEED = 0.03; + private static final double ROTATION_SPEED = 0.1; + private static final double BUTTON_ROTATION_SPEED = 0.1; + + private final XboxController controller; + + private final int tagId; + + private Pose3d tagPose; + private boolean tagHidden = true; + + public SimControlledTarget(int tagID, Pose3d startingPose, XboxController controller) { + this.tagId = tagID; + this.controller = controller; + + this.tagPose = startingPose; + } + + @Override + public void periodic() { + + tagHidden = controller.getAButton(); + + tagPose = + new Pose3d( + new Translation3d( + tagPose.getX() + - JoystickUtil.applyDeadband(controller.getLeftY()) * TRANSLATION_SPEED, + tagPose.getY() + - JoystickUtil.applyDeadband(controller.getLeftX()) * TRANSLATION_SPEED, + tagPose.getZ() + - controller.getLeftTriggerAxis() * TRIGGER_SPEED + + controller.getRightTriggerAxis() * TRIGGER_SPEED), + new Rotation3d( + MathUtil.angleModulus( + tagPose.getRotation().getX() * (controller.getPOV() == 0 ? 0 : 1) + - (controller.getPOV() == 270 ? BUTTON_ROTATION_SPEED : 0) + + (controller.getPOV() == 90 ? BUTTON_ROTATION_SPEED : 0)), + MathUtil.angleModulus( + tagPose.getRotation().getY() + + JoystickUtil.applyDeadband(controller.getRightY()) * ROTATION_SPEED), + MathUtil.angleModulus( + tagPose.getRotation().getZ() + - JoystickUtil.applyDeadband(controller.getRightX()) * ROTATION_SPEED))); + + Logger.recordOutput("SimControlledTarget/" + tagId + "/TagPose", tagPose); + Logger.recordOutput("SimControlledTarget/" + tagId + "/TagId", tagId); + } + + public AprilTagFieldLayout createFieldWithTarget(AprilTagFieldLayout initialField) { + + List tags = initialField.getTags(); + + tags.removeIf(tag -> tag.ID == tagId); + if (!tagHidden) { + tags.add(new AprilTag(tagId, tagPose)); + } + + return new AprilTagFieldLayout( + tags, initialField.getFieldLength(), initialField.getFieldWidth()); + } + + public int getTagId() { + return tagId; + } + + public Pose3d getTagPose() { + return tagPose; + } +} diff --git a/src/main/java/frc/robot/subsystems/vision/VisionConstants.java b/src/main/java/frc/robot/subsystems/vision/VisionConstants.java index e03ab5e..2ee4fc5 100644 --- a/src/main/java/frc/robot/subsystems/vision/VisionConstants.java +++ b/src/main/java/frc/robot/subsystems/vision/VisionConstants.java @@ -6,12 +6,17 @@ import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.math.geometry.Translation3d; import edu.wpi.first.math.util.Units; +import java.util.List; public class VisionConstants { // --- Vision Config --- - public static final AprilTagFieldLayout FIELD = - AprilTagFieldLayout.loadField(AprilTagFields.k2025ReefscapeWelded); + public static final AprilTagFieldLayout DEFAULT_FIELD = + AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField); + + public static final AprilTagFieldLayout BLANK_FIELD = + new AprilTagFieldLayout( + List.of(), DEFAULT_FIELD.getFieldLength(), DEFAULT_FIELD.getFieldWidth()); // Set cameraName on PhotonVision web interface. Edit camera name from camera type to camera // position. To find robotToCamera, measure the distance from the camera to the center of the @@ -19,20 +24,31 @@ public class VisionConstants { // Docs: https://docs.photonvision.org/en/latest/docs/apriltag-pipelines/coordinate-systems.html - public record CameraConfig(String cameraName, String cameraPosition, Transform3d robotToCamera) {} + enum CameraPositions { + FRONT, + LEFT, + RIGHT, + FRONT_LEFT, + FRONT_RIGHT, + BACK_LEFT, + BACK_RIGHT + } + + public record CameraConfig( + String cameraName, CameraPositions cameraPosition, Transform3d robotToCamera) {} public static final CameraConfig SIM_FRONT_CAMERA = new CameraConfig( "frontCam", - "front", + CameraPositions.FRONT, new Transform3d( new Translation3d(Units.inchesToMeters(27.5 / 2.0 + 1.0), 0, Units.inchesToMeters(6)), new Rotation3d(0, Units.degreesToRadians(0), 0))); - public static final CameraConfig WOODV2_LEFT_CAMERA = + public static final CameraConfig WOOD_V2_LEFT_CAMERA = new CameraConfig( "leftCamera", - "left", + CameraPositions.LEFT, new Transform3d( new Translation3d(0, Units.inchesToMeters(27.5 / 2.0 - 0.5), 0), new Rotation3d(0, Units.degreesToRadians(0), Units.degreesToRadians(90)))); @@ -40,7 +56,7 @@ public record CameraConfig(String cameraName, String cameraPosition, Transform3d public static final CameraConfig WOODV2_RIGHT_CAMERA = new CameraConfig( "rightCamera", - "right", + CameraPositions.RIGHT, new Transform3d( new Translation3d( 0, -Units.inchesToMeters(27.5 / 2.0 + 1.0), Units.inchesToMeters(3)), @@ -61,7 +77,7 @@ public record CameraConfig(String cameraName, String cameraPosition, Transform3d public static final CameraConfig COMP_FRONT_LEFT_CAMERA = new CameraConfig( "plzwork", - "Front Left", + CameraPositions.FRONT_LEFT, new Transform3d( new Translation3d(+CAMERA_OFFSET_X, +CAMERA_OFFSET_Y, FRONT_CAMERA_OFFSET_Z), new Rotation3d(0, -FRONT_CAMERA_PITCH, +FRONT_CAMERA_YAW))); @@ -69,7 +85,7 @@ public record CameraConfig(String cameraName, String cameraPosition, Transform3d public static final CameraConfig COMP_FRONT_RIGHT_CAMERA = new CameraConfig( "FrontRight8032", - "Front Right", + CameraPositions.FRONT_RIGHT, new Transform3d( new Translation3d(+CAMERA_OFFSET_X, -CAMERA_OFFSET_Y, FRONT_CAMERA_OFFSET_Z), new Rotation3d(0, -FRONT_CAMERA_PITCH, -FRONT_CAMERA_YAW))); @@ -77,7 +93,7 @@ public record CameraConfig(String cameraName, String cameraPosition, Transform3d public static final CameraConfig COMP_BACK_LEFT_CAMERA = new CameraConfig( "jarreaucam", - "Back Left", + CameraPositions.BACK_LEFT, new Transform3d( new Translation3d(-CAMERA_OFFSET_X, +CAMERA_OFFSET_Y, BACK_CAMERA_OFFSET_Z), new Rotation3d(0, -BACK_CAMERA_PITCH, Units.degreesToRadians(180 + 32.46)))); @@ -85,7 +101,7 @@ public record CameraConfig(String cameraName, String cameraPosition, Transform3d public static final CameraConfig COMP_BACK_RIGHT_CAMERA = new CameraConfig( "BackRightCamera8032", - "Back Right", + CameraPositions.BACK_RIGHT, new Transform3d( new Translation3d(-CAMERA_OFFSET_X, -CAMERA_OFFSET_Y, BACK_CAMERA_OFFSET_Z), new Rotation3d(0, -BACK_CAMERA_PITCH, Units.degreesToRadians(180 - 30)))); diff --git a/src/main/java/frc/robot/subsystems/visualizer/ObjectVisualizer.java b/src/main/java/frc/robot/subsystems/visualizer/ObjectVisualizer.java index 83e512d..baaaa7c 100644 --- a/src/main/java/frc/robot/subsystems/visualizer/ObjectVisualizer.java +++ b/src/main/java/frc/robot/subsystems/visualizer/ObjectVisualizer.java @@ -134,8 +134,11 @@ public Command placeHeldItemOnNearestWithInterpolation( List targets, double maxDistance, double seconds) { return defer( () -> { - Optional target = findHeldItemPlacementTarget(targets, maxDistance); - return placeHeldItemWithInterpolation(target.get(), seconds).onlyIf(target::isPresent); + Optional nearestTarget = findHeldItemPlacementTarget(targets, maxDistance); + if (nearestTarget.isPresent()) { + return placeHeldItemWithInterpolation(nearestTarget.get(), seconds); + } + return runOnce(this::ejectHeldItem); }); }