diff --git a/AdvantageScope RBSI Standard.json b/AdvantageScope RBSI Standard.json index 67dc05a9..da386f6e 100644 --- a/AdvantageScope RBSI Standard.json +++ b/AdvantageScope RBSI Standard.json @@ -10,14 +10,10 @@ "width": 300, "expanded": [ "/RealOutputs", - "/RealOutputs/LoopSpike/Drive", "/RealOutputs/Drive/OdomThread", "/RealOutputs/CAN/DriveTrain", "/RealOutputs/CAN/Module0", - "/RealOutputs/LoopSpike", "/AdvantageKit", - "/RealOutputs/Loop/Drive", - "/RealOutputs/Loop/Mech", "/RealOutputs/Odometry/Yaw", "/RealOutputs/Vision/Camera0", "/RealOutputs/Vision/Camera0/TagPoses", @@ -27,8 +23,15 @@ "/RealOutputs/Power", "/RealOutputs/Power/Subsystems", "/NetworkInputs/SmartDashboard", - "/RealOutputs/Loop", - "/RealOutputs/Loop/Virtual" + "/RealOutputs/LogPeriodic", + "/RealOutputs/LogPeriodic/CodeLoop", + "/RealOutputs/LogPeriodic/Subsystem", + "/RealOutputs/LogPeriodic/VirtualSubsystem", + "/RealOutputs/CAN/rio", + "/RealOutputs/Odometry/Coast", + "/RealOutputs/Drive/SparkOdomThread", + "/RealOutputs/Vision/Debug", + "/RealOutputs/Swerve/Drive" ] }, "tabs": { @@ -115,111 +118,819 @@ "renderer": null, "controlsHeight": 200 }, + { + "type": 1, + "title": "Vision Acceptance", + "controller": { + "leftSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera0/ObsSeen", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera0/ObsAccepted", + "logType": "Number", + "visible": true, + "options": { + "color": "#e5b31b", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera0/ObsRejected", + "logType": "Number", + "visible": true, + "options": { + "color": "#af2437", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera1/ObsSeen", + "logType": "Number", + "visible": true, + "options": { + "color": "#80588e", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera1/ObsAccepted", + "logType": "Number", + "visible": true, + "options": { + "color": "#e48b32", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera1/ObsRejected", + "logType": "Number", + "visible": true, + "options": { + "color": "#c0b487", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/TagCountThisLoop", + "logType": "Number", + "visible": true, + "options": { + "color": "#858584", + "size": "normal" + } + } + ], + "rightSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera0/Dbg_avgDist", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera0/Dbg_linearStdDev", + "logType": "Number", + "visible": true, + "options": { + "color": "#e5b31b", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera0/Dbg_angularStdDev", + "logType": "Number", + "visible": true, + "options": { + "color": "#af2437", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera1/Dbg_avgDist", + "logType": "Number", + "visible": true, + "options": { + "color": "#80588e", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera1/Dbg_linearStdDev", + "logType": "Number", + "visible": true, + "options": { + "color": "#e48b32", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Camera1/Dbg_angularStdDev", + "logType": "Number", + "visible": true, + "options": { + "color": "#c0b487", + "size": "normal" + } + } + ], + "discreteSources": [ + { + "type": "stripes", + "logKey": "/RealOutputs/Vision/HasAcceptedThisLoop", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#3b875a" + } + }, + { + "type": "stripes", + "logKey": "/RealOutputs/Vision/HasFusedThisLoop", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#d993aa" + } + }, + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#5f4528" + } + } + ], + "leftLockedRange": [ + 0, + 12 + ], + "rightLockedRange": null, + "leftUnitConversion": { + "autoTarget": null, + "preset": null + }, + "rightUnitConversion": { + "autoTarget": null, + "preset": null + }, + "leftFilter": 0, + "rightFilter": 0 + }, + "controllerUUID": "rbsivisionacceptance000000000000", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 2, + "title": "2D Field - Replay", + "controller": { + "sources": [ + { + "type": "robot", + "logKey": "/ReplayOutputs/Odometry/Robot", + "logType": "Pose2d", + "visible": true, + "options": { + "bumpers": "#ff0000" + } + }, + { + "type": "arrow", + "logKey": "/ReplayOutputs/Vision/Summary/TagPoses", + "logType": "Pose3d[]", + "visible": true, + "options": { + "position": "center" + } + }, + { + "type": "ghost", + "logKey": "/ReplayOutputs/Vision/Camera0/RobotPosesAccepted", + "logType": "Pose3d[]", + "visible": true, + "options": { + "color": "#00ff00" + } + } + ], + "field": "FRC:2026 Field", + "orientation": 2, + "size": "medium" + }, + "controllerUUID": "fcrb44yozcn5a3e1219jjhk9phytbtwd", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 2, + "title": "Real vs Replay", + "controller": { + "sources": [ + { + "type": "robot", + "logKey": "/RealOutputs/Odometry/Robot", + "logType": "Pose2d", + "visible": true, + "options": { + "bumpers": "#00ff00" + } + }, + { + "type": "robot", + "logKey": "/ReplayOutputs/Odometry/Robot", + "logType": "Pose2d", + "visible": true, + "options": { + "bumpers": "#ff8c00" + } + } + ], + "field": "FRC:2026 Field", + "orientation": 0, + "size": "medium" + }, + "controllerUUID": "hu1n2uavzveg9lf1r5wtmeyuylvia62n", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 1, + "title": "Odometry Health", + "controller": { + "leftSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Odometry/IMULatencySec", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Drive/PoseResetTimestamp", + "logType": "Number", + "visible": true, + "options": { + "color": "#e5b31b", + "size": "normal" + } + } + ], + "rightSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Drive/OdomThread/DroppedSamples", + "logType": "Number", + "visible": true, + "options": { + "color": "#af2437", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Drive/SparkOdomThread/DroppedSamples", + "logType": "Number", + "visible": true, + "options": { + "color": "#80588e", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Drive/PoseResetEpoch", + "logType": "Number", + "visible": true, + "options": { + "color": "#e48b32", + "size": "normal" + } + } + ], + "discreteSources": [ + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#3b875a" + } + } + ], + "leftLockedRange": [ + 0, + 0.1 + ], + "rightLockedRange": null, + "leftUnitConversion": { + "autoTarget": null, + "preset": null + }, + "rightUnitConversion": { + "autoTarget": null, + "preset": null + }, + "leftFilter": 0, + "rightFilter": 0 + }, + "controllerUUID": "rbsiodometryhealth00000000000000", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 1, + "title": "Disabled Pose Fusion", + "controller": { + "leftSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Odometry/Coast/maxWheelDeltaM", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Odometry/Coast/yawRateRadPerSec", + "logType": "Number", + "visible": true, + "options": { + "color": "#e5b31b", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Debug/dTransFromLastVision", + "logType": "Number", + "visible": true, + "options": { + "color": "#af2437", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Debug/dRotFromLastVision", + "logType": "Number", + "visible": true, + "options": { + "color": "#80588e", + "size": "normal" + } + } + ], + "rightSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Odometry/Coast/stationaryLoops", + "logType": "Number", + "visible": true, + "options": { + "color": "#e48b32", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/DisabledBlendAlphaUsed", + "logType": "Number", + "visible": true, + "options": { + "color": "#c0b487", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Vision/Debug/disabledCoastAge", + "logType": "Number", + "visible": true, + "options": { + "color": "#858584", + "size": "normal" + } + } + ], + "discreteSources": [ + { + "type": "stripes", + "logKey": "/RealOutputs/Odometry/Coast/active", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#2b66a2" + } + }, + { + "type": "stripes", + "logKey": "/RealOutputs/Vision/DisabledReject", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#af2437" + } + }, + { + "type": "stripes", + "logKey": "/RealOutputs/Vision/DisabledInitSnap", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#80588e" + } + }, + { + "type": "stripes", + "logKey": "/RealOutputs/Vision/Debug/disabledCoast", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#e48b32" + } + }, + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#d993aa" + } + } + ], + "leftLockedRange": null, + "rightLockedRange": null, + "leftUnitConversion": { + "autoTarget": null, + "preset": null + }, + "rightUnitConversion": { + "autoTarget": null, + "preset": null + }, + "leftFilter": 0, + "rightFilter": 0 + }, + "controllerUUID": "rbsidisabledposefusion0000000000", + "renderer": null, + "controlsHeight": 200 + }, { "type": 9, "title": "Swerve", "controller": { "sources": [ { - "type": "states", - "logKey": "/RealOutputs/SwerveStates/Measured", - "logType": "SwerveModuleState[]", + "type": "states", + "logKey": "/RealOutputs/SwerveStates/Measured", + "logType": "SwerveModuleState[]", + "visible": true, + "options": { + "color": "#ff0000", + "arrangement": "0,1,2,3" + } + }, + { + "type": "states", + "logKey": "/RealOutputs/SwerveStates/SetpointsOptimized", + "logType": "SwerveModuleState[]", + "visible": true, + "options": { + "color": "#00ffff", + "arrangement": "0,1,2,3" + } + }, + { + "type": "rotation", + "logKey": "/RealOutputs/Odometry/Robot/rotation", + "logType": "Rotation2d", + "visible": true, + "options": {} + }, + { + "type": "chassisSpeeds", + "logKey": "/RealOutputs/SwerveChassisSpeeds/Measured", + "logType": "ChassisSpeeds", + "visible": true, + "options": { + "color": "#ff0000" + } + } + ], + "maxSpeed": 5, + "sizeX": 0.65, + "sizeY": 0.65, + "orientation": 1 + }, + "controllerUUID": "zo4ukgbiguacvfe9e2iui2ytosl7gutn", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 1, + "title": "Drive Closed Loop", + "controller": { + "leftSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/Drive/VelocityRadPerSec", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/Drive/ClosedLoopVelocityRadPerSec", + "logType": "Number", + "visible": true, + "options": { + "color": "#e5b31b", + "size": "normal" + } + } + ], + "rightSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/Drive/AccelerationRotPerSec2", + "logType": "Number", + "visible": true, + "options": { + "color": "#af2437", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/Drive/ClosedLoopAccelRadPerSec2", + "logType": "Number", + "visible": true, + "options": { + "color": "#80588e", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/Drive/FeedForwardVolts", + "logType": "Number", + "visible": true, + "options": { + "color": "#e48b32", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/Drive/ClosedLoopFFVolts", + "logType": "Number", + "visible": true, + "options": { + "color": "#c0b487", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Swerve/BatteryVoltage", + "logType": "Number", + "visible": true, + "options": { + "color": "#858584", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/Robot/BatteryVoltage", + "logType": "Number", + "visible": true, + "options": { + "color": "#3b875a", + "size": "normal" + } + } + ], + "discreteSources": [ + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#d993aa" + } + } + ], + "leftLockedRange": null, + "rightLockedRange": null, + "leftUnitConversion": { + "autoTarget": null, + "preset": null + }, + "rightUnitConversion": { + "autoTarget": null, + "preset": null + }, + "leftFilter": 0, + "rightFilter": 0 + }, + "controllerUUID": "rbsidriveclosedloop0000000000000", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 1, + "title": "Power (V / A)", + "controller": { + "leftSources": [ + { + "type": "stepped", + "logKey": "/PowerDistribution/Voltage", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + } + ], + "rightSources": [ + { + "type": "stepped", + "logKey": "/PowerDistribution/TotalCurrent", + "logType": "Number", "visible": true, "options": { - "color": "#ff0000", - "arrangement": "0,1,2,3" + "color": "#e5b31b", + "size": "normal" } }, { - "type": "states", - "logKey": "/RealOutputs/SwerveStates/SetpointsOptimized", - "logType": "SwerveModuleState[]", + "type": "stepped", + "logKey": "/RealOutputs/Power/Subsystems/Drive_Current", + "logType": "Number", "visible": true, "options": { - "color": "#00ffff", - "arrangement": "0,1,2,3" + "color": "#80588e", + "size": "normal" } }, { - "type": "rotation", - "logKey": "/RealOutputs/Odometry/Robot/rotation", - "logType": "Rotation2d", + "type": "stepped", + "logKey": "/RealOutputs/Power/Subsystems/Steer_Current", + "logType": "Number", "visible": true, - "options": {} + "options": { + "color": "#e48b32", + "size": "normal" + } }, { - "type": "chassisSpeeds", - "logKey": "/RealOutputs/SwerveChassisSpeeds/Measured", - "logType": "ChassisSpeeds", + "type": "stepped", + "logKey": "/RealOutputs/Power/Subsystems/Flywheel_Current", + "logType": "Number", "visible": true, "options": { - "color": "#ff0000" + "color": "#c0b487", + "size": "normal" } } ], - "maxSpeed": 5, - "sizeX": 0.65, - "sizeY": 0.65, - "orientation": 1 + "discreteSources": [ + { + "type": "stripes", + "logKey": "/RealOutputs/Power/BrownoutImminent", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#af2437" + } + }, + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#3b875a" + } + } + ], + "leftLockedRange": [ + 0, + 14 + ], + "rightLockedRange": [ + 0, + 160 + ], + "leftUnitConversion": { + "autoTarget": null, + "preset": null + }, + "rightUnitConversion": { + "autoTarget": null, + "preset": null + }, + "leftFilter": 0, + "rightFilter": 0 }, - "controllerUUID": "zo4ukgbiguacvfe9e2iui2ytosl7gutn", + "controllerUUID": "rbsipowervamps000000000000000000", "renderer": null, "controlsHeight": 200 }, { "type": 1, - "title": "Power", + "title": "Battery Usage", "controller": { "leftSources": [ { "type": "stepped", - "logKey": "/PowerDistribution/Voltage", + "logKey": "/RealOutputs/Power/BatteryPercentEstimate", "logType": "Number", "visible": true, "options": { "color": "#2b66a2", "size": "normal" } - } - ], - "rightSources": [ + }, { "type": "stepped", - "logKey": "/PowerDistribution/TotalCurrent", + "logKey": "/RealOutputs/Power/AmpHoursUsed", "logType": "Number", "visible": true, "options": { "color": "#e5b31b", "size": "normal" } - }, + } + ], + "rightSources": [ { "type": "stepped", - "logKey": "/RealOutputs/Power/Subsystems/DriveCurrent", + "logKey": "/RealOutputs/Power/TotalPower", "logType": "Number", "visible": true, "options": { - "color": "#80588e", + "color": "#af2437", "size": "normal" } }, { "type": "stepped", - "logKey": "/RealOutputs/Power/Subsystems/SteerCurrent", + "logKey": "/RealOutputs/Power/EnergyWh", "logType": "Number", "visible": true, "options": { - "color": "#e48b32", + "color": "#80588e", "size": "normal" } }, { "type": "stepped", - "logKey": "/RealOutputs/Power/Subsystems/FlywheelCurrent", + "logKey": "/RealOutputs/Power/EnergyJoules", "logType": "Number", "visible": true, "options": { - "color": "#c0b487", + "color": "#e48b32", "size": "normal" } } @@ -231,11 +942,23 @@ "logType": "Boolean", "visible": true, "options": { - "color": "#af2437" + "color": "#c0b487" + } + }, + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#3b875a" } } ], - "leftLockedRange": null, + "leftLockedRange": [ + 0, + 100 + ], "rightLockedRange": null, "leftUnitConversion": { "autoTarget": null, @@ -248,18 +971,18 @@ "leftFilter": 0, "rightFilter": 0 }, - "controllerUUID": "9vyygyem9we0y5917klbu25cz9hruszx", + "controllerUUID": "rbsibatteryusage0000000000000000", "renderer": null, "controlsHeight": 200 }, { "type": 1, - "title": "CAN Health", + "title": "CAN Health (utilization / counts)", "controller": { "leftSources": [ { "type": "stepped", - "logKey": "/RealOutputs/CAN//Utilization", + "logKey": "/RealOutputs/CAN/rio/Utilization", "logType": "Number", "visible": true, "options": { @@ -281,7 +1004,7 @@ "rightSources": [ { "type": "stepped", - "logKey": "/RealOutputs/CAN/DriveTrain/RxErrorCount", + "logKey": "/RealOutputs/CAN/rio/RxErrorCount", "logType": "Number", "visible": true, "options": { @@ -291,7 +1014,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/CAN/DriveTrain/TxErrorCount", + "logKey": "/RealOutputs/CAN/rio/TxErrorCount", "logType": "Number", "visible": true, "options": { @@ -301,7 +1024,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/CAN//RxErrorCount", + "logKey": "/RealOutputs/CAN/rio/BusOffCount", "logType": "Number", "visible": true, "options": { @@ -311,17 +1034,50 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/CAN/DriveTrain/TxFullCount", + "logKey": "/RealOutputs/CAN/DriveTrain/RxErrorCount", "logType": "Number", "visible": true, "options": { "color": "#c0b487", "size": "normal" } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/CAN/DriveTrain/TxErrorCount", + "logType": "Number", + "visible": true, + "options": { + "color": "#858584", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/CAN/DriveTrain/TxFullCount", + "logType": "Number", + "visible": true, + "options": { + "color": "#3b875a", + "size": "normal" + } } ], - "discreteSources": [], - "leftLockedRange": null, + "discreteSources": [ + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#d993aa" + } + } + ], + "leftLockedRange": [ + 0, + 1 + ], "rightLockedRange": null, "leftUnitConversion": { "autoTarget": null, @@ -334,19 +1090,18 @@ "leftFilter": 0, "rightFilter": 0 }, - "controllerUUID": "f15s8kcvuqce63dlc7cf4yunt6c0372w", + "controllerUUID": "rbsicanhealth0000000000000000000", "renderer": null, "controlsHeight": 200 }, { "type": 1, - "title": "Loop Time - Robot", + "title": "Loop Time - Robot (ms)", "controller": { - "leftSources": [], - "rightSources": [ + "leftSources": [ { "type": "stepped", - "logKey": "/RealOutputs/LoggedRobot/FullCycleMS", + "logKey": "/RealOutputs/LogPeriodic/CodeLoop/RobotPeriodicMS", "logType": "Number", "visible": true, "options": { @@ -356,7 +1111,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/LoggedRobot/GCTimeMS", + "logKey": "/RealOutputs/LogPeriodic/CodeLoop/VirtualMS", "logType": "Number", "visible": true, "options": { @@ -366,7 +1121,19 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/LoggedRobot/LogPeriodicMS", + "logKey": "/RealOutputs/LogPeriodic/CodeLoop/SchedulerMS", + "logType": "Number", + "visible": true, + "options": { + "color": "#af2437", + "size": "normal" + } + } + ], + "rightSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/LoggedRobot/FullCycleMS", "logType": "Number", "visible": true, "options": { @@ -380,13 +1147,107 @@ "logType": "Number", "visible": true, "options": { - "color": "#af2437", + "color": "#e48b32", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/LoggedRobot/LogPeriodicMS", + "logType": "Number", + "visible": true, + "options": { + "color": "#c0b487", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/LoggedRobot/GCTimeMS", + "logType": "Number", + "visible": true, + "options": { + "color": "#858584", "size": "normal" } } ], - "discreteSources": [], - "leftLockedRange": null, + "discreteSources": [ + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#3b875a" + } + } + ], + "leftLockedRange": [ + 0, + 25 + ], + "rightLockedRange": [ + 0, + 25 + ], + "leftUnitConversion": { + "autoTarget": null, + "preset": null + }, + "rightUnitConversion": { + "autoTarget": null, + "preset": null + }, + "leftFilter": 0, + "rightFilter": 0 + }, + "controllerUUID": "rbsilooprobotms00000000000000000", + "renderer": null, + "controlsHeight": 200 + }, + { + "type": 1, + "title": "Loop Time - Mechanisms (ms)", + "controller": { + "leftSources": [ + { + "type": "stepped", + "logKey": "/RealOutputs/LogPeriodic/Subsystem/DriveMS", + "logType": "Number", + "visible": true, + "options": { + "color": "#2b66a2", + "size": "normal" + } + }, + { + "type": "stepped", + "logKey": "/RealOutputs/LogPeriodic/Subsystem/FlywheelMS", + "logType": "Number", + "visible": true, + "options": { + "color": "#e5b31b", + "size": "normal" + } + } + ], + "rightSources": [], + "discreteSources": [ + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#af2437" + } + } + ], + "leftLockedRange": [ + 0, + 25 + ], "rightLockedRange": null, "leftUnitConversion": { "autoTarget": null, @@ -399,18 +1260,18 @@ "leftFilter": 0, "rightFilter": 0 }, - "controllerUUID": "yktsi99006yw1sq2h8mihr8kiy9h5xk9", + "controllerUUID": "rbsiloopmechms000000000000000000", "renderer": null, "controlsHeight": 200 }, { "type": 1, - "title": "Loop Time - Subsystems", + "title": "Loop Time - Virtual (ms)", "controller": { "leftSources": [ { "type": "stepped", - "logKey": "/RealOutputs/Loop/Mech/Drive_ms", + "logKey": "/RealOutputs/LogPeriodic/VirtualSubsystem/ImuMS", "logType": "Number", "visible": true, "options": { @@ -420,7 +1281,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/Loop/Mech/Flywheel_ms", + "logKey": "/RealOutputs/LogPeriodic/VirtualSubsystem/DriveOdometryMS", "logType": "Number", "visible": true, "options": { @@ -430,7 +1291,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/Loop/Virtual/Accelerometer_ms", + "logKey": "/RealOutputs/LogPeriodic/VirtualSubsystem/VisionMS", "logType": "Number", "visible": true, "options": { @@ -440,7 +1301,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/Loop/Virtual/RBSICANHealth_ms", + "logKey": "/RealOutputs/LogPeriodic/VirtualSubsystem/AccelerometerMS", "logType": "Number", "visible": true, "options": { @@ -450,7 +1311,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/Loop/Virtual/RBSIPowerMonitor_ms", + "logKey": "/RealOutputs/LogPeriodic/VirtualSubsystem/RBSICANHealthMS", "logType": "Number", "visible": true, "options": { @@ -460,7 +1321,7 @@ }, { "type": "stepped", - "logKey": "/RealOutputs/Loop/Virtual/Vision_ms", + "logKey": "/RealOutputs/LogPeriodic/VirtualSubsystem/RBSIPowerMonitorMS", "logType": "Number", "visible": true, "options": { @@ -470,8 +1331,21 @@ } ], "rightSources": [], - "discreteSources": [], - "leftLockedRange": null, + "discreteSources": [ + { + "type": "stripes", + "logKey": "/DriverStation/Enabled", + "logType": "Boolean", + "visible": true, + "options": { + "color": "#858584" + } + } + ], + "leftLockedRange": [ + 0, + 25 + ], "rightLockedRange": null, "leftUnitConversion": { "autoTarget": null, @@ -484,9 +1358,9 @@ "leftFilter": 0, "rightFilter": 0 }, - "controllerUUID": "no6ewy1a2lc06qvs7einn5i430wwub9n", + "controllerUUID": "rbsiloopvirtualms000000000000000", "renderer": null, - "controlsHeight": 195 + "controlsHeight": 200 }, { "type": 3, @@ -530,6 +1404,14 @@ ] }, "controlsHeight": 200 + }, + { + "type": 7, + "title": "Video", + "controller": null, + "controllerUUID": "vnanjmnujfndic5ztjusxrarh2m76p4x", + "renderer": null, + "controlsHeight": 85 } ] } @@ -537,5 +1419,5 @@ } ], "satellites": [], - "version": "26.0.0" + "version": "26.0.2" } diff --git a/README.md b/README.md index 36c77162..ecad12cc 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,104 @@ [![CI](https://github.com/AZ-First/Az-RBSI/actions/workflows/ci.yaml/badge.svg)](https://github.com/AZ-First/Az-RBSI/actions/workflows/ci.yaml) - ![AzFIRST Logo](https://github.com/AZ-First/Az-RBSI/blob/main/AZ-First-logo.png?raw=true) # Az-RBSI -Arizona's Reference Build and Software Implementation for FRC Robots (read: "A-Z-ribsy") - - -## Installation - -Installation instructions are found in the [INSTALL.md](doc/INSTALL.md) file, and the [Getting -Started Guide](doc/RBSI-GSG.md) includes the steps you'll need to do before taking your robot -out for a spin. See the [Releases Page](https://github.com/AZ-First/Az-RBSI/releases) for -details on the latest release, including restrictions and cautions. +Arizona's Reference Build and Software Implementation for FRC robots, read as +"A-Z-ribsy". + +Az-RBSI is a robot-code template for teams that want a reliable swerve, +odometry, vision, autonomous, logging, and tuning baseline without starting from +an empty project. + +## Documentation + +Start with the full documentation index: + +- [doc/README.md](doc/README.md) + +Quick links: + +- [Install and project setup](doc/INSTALL.md) +- [Getting started guide](doc/RBSI-GSG.md) +- [Constants guide](doc/RBSI-Constants.md) +- [Drive subsystem and Phoenix/YAGSL setup](doc/RBSI-Drive.md) +- [Vision setup and troubleshooting](doc/RBSI-Vision.md) +- [Autonomous setup](doc/RBSI-Autonomous.md) +- [SysId guide](doc/RBSI-SysId.md) +- [Pose buffer design notes](doc/RBSI-PoseBuffer.md) +- [AdvantageScope layout guide](doc/RBSI-AdvantageScope.md) + +## New Team Flow + +1. Read [INSTALL.md](doc/INSTALL.md) and create your project. +2. Follow [RBSI-GSG.md](doc/RBSI-GSG.md) for first robot-code edits. +3. Configure `Constants.java` with [RBSI-Constants.md](doc/RBSI-Constants.md). +4. Configure the drivetrain with [RBSI-Drive.md](doc/RBSI-Drive.md). +5. Bring up cameras with [RBSI-Vision.md](doc/RBSI-Vision.md). +6. Add autonomous paths with [RBSI-Autonomous.md](doc/RBSI-Autonomous.md). +7. Open the standard layout with [RBSI-AdvantageScope.md](doc/RBSI-AdvantageScope.md). +8. Characterize mechanisms with [RBSI-SysId.md](doc/RBSI-SysId.md). + +## Experienced User Shortcuts + +- Phoenix Tuner X generated constants: + [RBSI-Drive.md](doc/RBSI-Drive.md#phoenix-tuner-x-constants) and + [`src/main/java/frc/robot/generated/README`](src/main/java/frc/robot/generated/README) +- PathPlanner and Choreo lifecycle: + [RBSI-Autonomous.md](doc/RBSI-Autonomous.md#match-execution-flow) +- Disabled odometry and vision behavior: + [RBSI-PoseBuffer.md](doc/RBSI-PoseBuffer.md) +- Camera transforms, filtering, and simulation: + [RBSI-Vision.md](doc/RBSI-Vision.md) +- Constants tuning order: + [RBSI-Constants.md](doc/RBSI-Constants.md#recommended-tuning-order-for-a-new-robot) +- AdvantageScope pit-debug layout: + [RBSI-AdvantageScope.md](doc/RBSI-AdvantageScope.md) ## Purpose -The purpose of Az-RBSI is to help Arizona FRC teams with: -* Improving robot reliability / performance during “Autonomous Play” -* Improving robot build & endurance, gameplay reliability and troubleshooting - skills -* Providing a standardized robot “stack” to allow for quick software setup and - troubleshooting, and make it easier for Arizona teams to form effective - in-state alliances +Az-RBSI helps FRC teams with: +- improving autonomous reliability and performance, +- improving robot build, endurance, gameplay reliability, and troubleshooting, +- standardizing a robot stack so teams can set up software quickly, +- making it easier for Arizona teams to form effective in-state alliances. ## Design Philosophy -The Az-RBSI is centered around a "Reference Build" robot that allows for teams -to communicate quickly and effectively with each other about gameplay strategy -and troubleshooting. Additionally, the consolidation around a standard robot -design allows for easier swapping of spare parts and programming modules. - -The Az-RBSI software is an outline of an FRC robot program upon which teams can -build with their particular mechanisms and designs. It weaves together popular -and currently maintained FIRST- and community-sponsored software libraries to -provide a baseline robot functionality that combines robust reliability with -effective logging for troubleshooting. +Az-RBSI is centered around a reference robot that helps teams communicate +quickly about gameplay strategy and troubleshooting. A shared robot design also +makes it easier to swap spare parts and programming modules. +The software is a robot-program outline that teams can extend for their own +mechanisms and game strategy. It combines actively maintained FIRST and +community libraries with AdvantageKit logging so teams can diagnose problems +from real match data. ## Library Dependencies -* [WPILib](https://docs.wpilib.org/en/stable/index.html) -- FIRST basic libraries -* [AdvantageKit]( - https://docs.advantagekit.org/getting-started/what-is-advantagekit/) - -- Logging -* [CTRE Phoenix6]( - https://v6.docs.ctr-electronics.com/en/stable/docs/api-reference/mechanisms/swerve/swerve-overview.html) -- Swerve drive library -* [PathPlanner](https://pathplanner.dev/home.html) -- Autonomous path planning -* [PhotonVision](https://docs.photonvision.org/en/latest/) / [Limelight]( - https://docs.limelightvision.io/docs/docs-limelight/getting-started/summary) - -- Robot vision / tracking -* [Autopilot](https://therekrab.github.io/autopilot/index.html) -- Drive-to-Pose semi-autonomous movements - -## FRC Kickoff Workshop Slides - -### 2026 - REBUILT - -Google Drive links for our 30-minute 2026 Kickoff Workshops: - -* [AZ RBSI and Advantage Kit -](https://docs.google.com/presentation/d/1KOfODbdGbk8L_G25i7iYnaahoKr_Tzg54LJYN4yax_4/edit?usp=sharing) -* [Know Where You Are: PhotonVision for Alignment and Odometry -](https://docs.google.com/presentation/d/1JWYmwpZYA2zBuNIj9kKBUC_O-i0d1-SW_6qsVxgPdCA/edit?usp=sharing) +- [WPILib](https://docs.wpilib.org/en/stable/index.html): FIRST robot libraries +- [AdvantageKit](https://docs.advantagekit.org/getting-started/what-is-advantagekit/): logging +- [CTRE Phoenix 6](https://v6.docs.ctr-electronics.com/en/stable/docs/api-reference/mechanisms/swerve/swerve-overview.html): CTRE swerve +- [PathPlanner](https://pathplanner.dev/home.html): autonomous path planning +- [PhotonVision](https://docs.photonvision.org/en/latest/) and + [Limelight](https://docs.limelightvision.io/docs/docs-limelight/getting-started/summary): + robot vision +- [Autopilot](https://therekrab.github.io/autopilot/index.html): teleop drive-to-pose +## Workshop Slides -### 2025 - Reefscape +2026 REBUILT kickoff workshops: -Google Drive link for our 2-hour 2025 Kickoff Workshop introducing Az-RBSI: +- [AZ RBSI and AdvantageKit](https://docs.google.com/presentation/d/1KOfODbdGbk8L_G25i7iYnaahoKr_Tzg54LJYN4yax_4/edit?usp=sharing) +- [Know Where You Are: PhotonVision for Alignment and Odometry](https://docs.google.com/presentation/d/1JWYmwpZYA2zBuNIj9kKBUC_O-i0d1-SW_6qsVxgPdCA/edit?usp=sharing) -* [AZ Liftoff RBSI](https://docs.google.com/presentation/d/1c8A5RlPeEvKcj9yC66Ffvh5Os6jWyZiACoSRjDDETUs/edit?usp=sharing) +2025 Reefscape kickoff workshop: +- [AZ Liftoff RBSI](https://docs.google.com/presentation/d/1c8A5RlPeEvKcj9yC66Ffvh5Os6jWyZiACoSRjDDETUs/edit?usp=sharing) ## Further Reading -For tips on command-based programming, see this post: -https://bovlb.github.io/frc-tips/commands/best-practices.html +- [Command-based best practices](https://bovlb.github.io/frc-tips/commands/best-practices.html) +- [RBSI releases](https://github.com/AZ-First/Az-RBSI/releases) diff --git a/build.gradle b/build.gradle index 0584df98..e548ab87 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,14 @@ java { targetCompatibility = JavaVersion.VERSION_17 } +sourceSets { + test { + java { + srcDirs = ['src/test'] + } + } +} + def ROBOT_MAIN_CLASS = "frc.robot.Main" // Define my targets (RoboRIO) and artifacts (deployable files) @@ -91,6 +99,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text) + implementation "org.littletonrobotics.akit:akit-java:$akitJson.version" annotationProcessor "org.littletonrobotics.akit:akit-autolog:$akitJson.version" } diff --git a/doc/INSTALL.md b/doc/INSTALL.md index ac19b1c4..b03afb82 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -1,28 +1,31 @@ # Az-RBSI Installation Instructions -### Pre install -Before you even think about Az-RBSI, you need these _minimum_ versions of the +### Pre-Install +Before you even think about Az-RBSI, you need these _minimum_ versions of the following components on your laptop and devices. -* WPILib ` v2026.2.1` +* WPILib `v2026.2.1` * RoboRIO image `FRC_roboRIO_2026_v1.2` (comes with the FRC Game Tools from National Instruments) * Driver Station `Version 26.0` (comes with the FRC Game Tools from National Instruments) -* CTRE Tunner X `26.2.4.0`, with all devices running firmware `26.0` or newer. - This includes all motors, CANivore, Pigeon 2.0, and all CANcoders! -* Rev Hardware Client `2.0`, with the PDH and all SparkMax's, and other devices +* CTRE Tuner X `26.2.4.0`, with all devices running firmware `26.0` or newer. + This includes all motors, CANivore, Pigeon 2.0, and all CANcoders. +* REV Hardware Client `2.0`, with the PDH, all SPARK MAXs, and other devices running firmware `26.1` or newer. * Vivid Hosting Radio firmware `2.0.1` or newer is required for competition this year. * Photon Vision ([Orange Pi or other device](https://docs.photonvision.org/en/latest/docs/quick-start/quick-install.html)) - **running `26.1` or newer** (make sure you are **not** acidentially running - `25.3`). We HIGHLY recomend downloading the image and re-imaging the SD Card - in your co-processor instead of trying to upgrade it. + **running `26.1` or newer** (make sure you are **not** accidentally running + `25.3`). We strongly recommend downloading the image and re-imaging the SD + card in your co-processor instead of trying to upgrade it. -It is highly recommmended to update all you devices, and label what can id's or ip adresses and firmware versions they are running. This helps your team, and the FRC field staff quickly identify issues. +Update all of your devices and label each device with its CAN ID or IP address +and firmware version. This helps your team and FRC field staff identify issues +quickly. -If you are running a RoboRIO 1.0 (no sd card) you also neeed to disable the web server ([Instructions Here](https://docs.wpilib.org/en/stable/docs/software/wpilib-tools/roborio-team-number-setter/index.html)) +If you are running a RoboRIO 1.0 (no SD card), you also need to disable the web +server ([instructions here](https://docs.wpilib.org/en/stable/docs/software/wpilib-tools/roborio-team-number-setter/index.html)). -------- @@ -40,12 +43,13 @@ already have a GitHub account where you will store your 2026 FRC robot code. ### Creating a 2026 FRC project from the Az-RBSI Template -From the [Az-RBSI GiuHub page](https://github.com/AZ-First/Az-RBSI/), click the +From the [Az-RBSI GitHub page](https://github.com/AZ-First/Az-RBSI/), click the "Use this template" button in the upper right corner of the page. In the page that opens, select the Owner (most likely your team's account) and Repository name (*e.g.*, "FRC-2026" or "REBUILT Robot Code" or whatever your -team's naming convention is) into which the create the new robot project. +team's naming convention is) into which GitHub will create the new robot +project. Optionally, include a description of the repository for your reference. Select "public" or "private" repository based on the usual practices of your team. @@ -56,7 +60,8 @@ If you want to keep caught up on dependencies, you will need to ENABLE the Dependency Graph selection under the "Advanced Security" tab of the repository Settings. -* If you are struggling with this step, you may need the mentor or teacher that owns your github org to to it. +* If you are struggling with this step, you may need the mentor or teacher who + owns your GitHub organization to do it. Enable Dependency Graph @@ -74,8 +79,6 @@ https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/index.html). Take a moment to update all software and firmware to the latest versions before attempting to load your new robot project. - - -------- ### Setting up your new project @@ -105,27 +108,58 @@ steps you need to complete: ``` 2. If you have an all-CTRE swerve base (*i.e.*, 8x TalonFX-controlled motors, - 4x CANCoders, and 1x Pigeon2), use Phoenix Tuner X to create a swerve - project. Follow the instructions in the Phoenix documentation for the + 4x CANcoders, and 1x Pigeon2), use Phoenix Tuner X to create a swerve + project. Follow the instructions in CTRE's [Tuner X Swerve Project Generator]( https://v6.docs.ctr-electronics.com/en/latest/docs/tuner/tuner-swerve/index.html). - This will generate the correct offsets and inversions for your drive train. + This generates the measured module offsets, module locations, device IDs, + inversions, gear ratios, and base Phoenix 6 swerve constants for your drive + train. + +3. On the final screen in Tuner X, choose the option that generates only + `TunerConstants.java`. + +4. Copy that generated file into `src/main/java/frc/robot/generated/`, then + rename it for the RBSI robot selected in `Constants.java`: + + - `COMPBOTTunerConstants.java` for `Constants.RobotType.COMPBOT` + - `DEVBOT1TunerConstants.java` for `Constants.RobotType.DEVBOT1` + - `DEVBOT2TunerConstants.java` for `Constants.RobotType.DEVBOT2` + + Also update the class name inside the file. For example, if you copied the + file to `COMPBOTTunerConstants.java`, the declaration must be: + + ```java + public class COMPBOTTunerConstants { + ``` + +5. In the copied `*TunerConstants.java` file, comment out the generated import + for `CommandSwerveDrivetrain`. RBSI does not use CTRE's generated command + drivetrain class. + +6. In the same file, comment out the generated `createDrivetrain()` function. + RBSI constructs the drivebase through `frc.robot.subsystems.drive.Drive`, + then reads the generated `DrivetrainConstants`, `FrontLeft`, `FrontRight`, + `BackLeft`, and `BackRight` constants through the RBSI view classes. -3. On the final screen in Tuner X, choose "Generate only TunerConstants" and - overwrite the file located at `src/main/java/frc/robot/generated/TunerConstants.java`. +7. Make sure the matching `*TunerView.java` file still points at the generated + constants file you copied. For example, `COMPBOTTunerView` should return + `COMPBOTTunerConstants.kCANBus`, `COMPBOTTunerConstants.DrivetrainConstants`, + and the four public module constants. `TunerFactory` selects the right view + from `Constants.getRobot()`, so normal drive code does not import a + per-robot TunerConstants class directly. -4. In `TunerConstants.java`, comment out the [last import]( - https://github.com/CrossTheRoadElec/Phoenix6-Examples/blob/1db713d75b08a4315c9273cebf5b5e6a130ed3f7/java/SwerveWithPathPlanner/src/main/java/frc/robot/generated/TunerConstants.java#L18) - and [last method]( - https://github.com/CrossTheRoadElec/Phoenix6-Examples/blob/1db713d75b08a4315c9273cebf5b5e6a130ed3f7/java/SwerveWithPathPlanner/src/main/java/frc/robot/generated/TunerConstants.java#L171-L175). - Before removing them, both lines will be marked as errors in VSCode. +8. In the copied `*TunerConstants.java`, review `kSlipCurrent`. A conservative + starting point is `60` amps; tune it on the real robot and event carpet. -5. In `TunerConstants.java`, change `kSlipCurrent` to `60` amps. This will - keep your robot from tearing holes in the carpet at competition! +9. In the copied `*TunerConstants.java`, review `kSteerInertia` and + `kDriveInertia`. The generic RBSI simulation expects values close to + `0.004` and `0.025`, respectively, unless you have better measured values. -6. In `TunerConstants.java`, change `kSteerInertia` to `0.004` and - `kDriveInertia` to `0.025` to allow the AdvantageKit simulation code to - operate as expected. +10. Open [RBSI-Constants.md](RBSI-Constants.md) and work through the sections + that match your robot. At minimum, verify `RobotDevices`, + `DrivebaseConstants`, `OperatorConstants`, `AutoConstants`, and + `VisionConstants` before your first serious drive test. **NOTE:** If you have any other combination of hardware (including REV NEOs, @@ -139,9 +173,11 @@ repository](https://github.com/AZ-First/Az-RBSI). -------- -### Getting Started with your Robot Code +### Getting Started with Your Robot Code -See the Az-RBSI [Getting Started Guide](RBSI-GSG.md) for next steps. +See the Az-RBSI [Getting Started Guide](RBSI-GSG.md) for next steps. The +[documentation index](README.md) also links the drivetrain, vision, autonomous, +constants, pose-buffer, and SysId guides. -------- diff --git a/doc/RBSI-AdvantageScope.md b/doc/RBSI-AdvantageScope.md new file mode 100644 index 00000000..8c1e472f --- /dev/null +++ b/doc/RBSI-AdvantageScope.md @@ -0,0 +1,285 @@ +# RBSI AdvantageScope Layout + +This page describes `AdvantageScope RBSI Standard.json`, the standard +AdvantageScope layout included with RBSI. + +Use this layout when reviewing logs from practice, matches, replay, and pit +debugging. It is intentionally generic: it focuses on drivetrain, odometry, +vision, power, CAN health, loop timing, and the example flywheel. + +## Loading The Layout + +1. Open AdvantageScope. +2. Open a live robot connection or a `.wpilog`. +3. Use AdvantageScope's layout import/open command. +4. Select `AdvantageScope RBSI Standard.json` from the repository root. + +The companion `AdvantageScope Swerve Calibration.json` layout is narrower and +intended for swerve setup and calibration. + +## Tabs + +### 2D Field - Odometry + +Shows `/RealOutputs/Odometry/Robot` on the 2026 field. Use this first when +checking whether the robot pose moves correctly from wheel odometry and gyro +alone. + +### 2D Field - Vision + +Overlays robot odometry, visible AprilTag poses, and accepted camera robot-pose +observations. Use this to confirm camera transforms and tag filtering. + +### Vision Acceptance + +Plots per-camera observation counts and vision trust diagnostics: + +- observations seen, accepted, and rejected, +- tag count this loop, +- average tag distance, +- linear and angular standard deviations, +- accepted/fused boolean stripes. + +Use this tab when vision appears connected but pose is not updating. + +### 2D Field - Replay + +Shows replay-output odometry and replay-output vision observations. Use this +when running AdvantageKit replay to compare simulated/replayed pose behavior +against real match data. + +### Real vs Replay + +Overlays real and replay robot poses. This is useful when validating replay +changes to odometry, pose fusion, or autonomous behavior. + +### Odometry Health + +Plots odometry support signals: + +- IMU latency seconds, +- Phoenix odometry dropped samples, +- Spark odometry dropped samples, +- pose reset timestamp, +- pose reset epoch. + +Use this tab when pose updates stutter, replay diverges, or vision appears to be +rejected after pose resets. + +### Disabled Pose Fusion + +Plots disabled coast and disabled vision-fusion behavior: + +- disabled coast active, +- stationary loop count, +- max wheel delta in meters, +- yaw rate in radians per second, +- disabled vision blend alpha, +- disabled vision reject/init-snap booleans. + +Use this when the robot is disabled on the field and vision is expected to pull +pose gently toward tag observations after the robot stops coasting. + +### Swerve + +Shows measured, setpoint, and optimized swerve module states. Use this for +module direction, optimization, and drive command debugging. + +### Drive Closed Loop + +Plots drive closed-loop support signals where available: + +- drive velocity in radians per second, +- closed-loop drive velocity in radians per second, +- drive acceleration, +- feedforward voltage, +- battery voltage. + +Some signals are produced only by specific IO implementations. For example, +Phoenix and Spark paths do not log every identical helper key. + +### Power (V / A) + +Separates voltage and current onto different axes: + +- PDH/PDP voltage, +- total current, +- drive current, +- steer current, +- example flywheel current, +- brownout-imminent stripe. + +Use this during pit checks to find low battery voltage, current spikes, or an +unexpectedly expensive mechanism. + +### Battery Usage + +Plots higher-level battery estimates: + +- battery percent estimate, +- amp-hours used, +- total power, +- energy in watt-hours and joules, +- brownout-imminent stripe. + +Use this as a trend view across practice runs or long troubleshooting sessions. + +### CAN Health (utilization / counts) + +Separates CAN utilization from error counts: + +- roboRIO bus utilization, +- drivetrain CAN bus utilization, +- receive/transmit error counts, +- bus-off counts, +- TX full count, +- enabled stripe. + +Utilization is locked to a `0..1` range because CTRE reports it as a fraction. +Error counts are left unlocked so spikes are visible. + +### Loop Time - Robot (ms) + +Plots robot-level loop timing in milliseconds: + +- full robot cycle, +- user code, +- log periodic, +- garbage collection, +- RBSI code-loop timing split into virtual subsystem and command scheduler + portions. + +Use this first when chasing 20 ms hot-loop issues. + +### Loop Time - Mechanisms (ms) + +Plots `RBSISubsystem` timing in milliseconds. The template includes: + +- `DriveMS`, +- `FlywheelMS`. + +Add mechanism subsystem timing here as you create robot-specific mechanisms. + +### Loop Time - Virtual (ms) + +Plots `VirtualSubsystem` timing in milliseconds: + +- `ImuMS`, +- `DriveOdometryMS`, +- `VisionMS`, +- `AccelerometerMS`, +- `RBSICANHealthMS`, +- `RBSIPowerMonitorMS`. + +Use this when virtual processing, vision, or health monitoring is suspected of +slowing the main loop. + +### 3D Field + +Shows 3D robot and vision objects. Use this to spot obvious transform mistakes +such as cameras below the floor, tags in the wrong place, or the robot rotated +incorrectly. + +### Video + +Reserved for AdvantageScope video sync. Use it when match video is available +and aligned with a `.wpilog`. + +## Adding Mechanisms To Power + +Power logging is automatic only after the mechanism reports its PDH/PDP ports +and is passed to `RBSIPowerMonitor`. + +1. Add mechanism device IDs and power ports in `Constants.RobotDevices`. + + ```java + public static final RobotDeviceId ARM_LEADER = new RobotDeviceId(20, CANBuses.RIO, 5); + public static final RobotDeviceId ARM_FOLLOWER = new RobotDeviceId(21, CANBuses.RIO, 6); + ``` + +2. In the mechanism IO implementation, return those ports. + + ```java + @Override + public int[] powerPorts() { + return new int[] { + RobotDevices.ARM_LEADER.getPowerPort(), + RobotDevices.ARM_FOLLOWER.getPowerPort() + }; + } + ``` + +3. In the subsystem, forward IO power ports through `getPowerPorts()`. + + ```java + @Override + public int[] getPowerPorts() { + return io.getPowerPorts(); + } + ``` + +4. In `RobotContainer`, pass the subsystem to `RBSIPowerMonitor`. + + ```java + m_power = new RBSIPowerMonitor(batteryCapacity, m_flywheel, m_arm); + ``` + +5. In AdvantageScope, add the logged key to `Power (V / A)`: + + ```text + /RealOutputs/Power/Subsystems/Arm_Current + ``` + +The subsystem name comes from `getClass().getSimpleName()`. If the class is +`Arm`, the power key is `Arm_Current`. If the class is `CoralIntake`, the key is +`CoralIntake_Current`. + +## Adding Mechanisms To Loop Timing + +Mechanism timing is automatic for classes that extend `RBSISubsystem` and put +their periodic work in `rbsiPeriodic()`. + +1. Make the mechanism extend `RBSISubsystem`. + + ```java + public class Arm extends RBSISubsystem { + @Override + protected void rbsiPeriodic() { + io.updateInputs(inputs); + Logger.processInputs("Arm", inputs); + } + } + ``` + +2. Do not override `periodic()`. RBSI owns `periodic()` so it can time every + subsystem consistently. + +3. After deploying, add the mechanism timing key to `Loop Time - Mechanisms + (ms)`: + + ```text + /RealOutputs/LogPeriodic/Subsystem/ArmMS + ``` + +Again, the key uses the Java class simple name. `CoralIntake` becomes: + +```text +/RealOutputs/LogPeriodic/Subsystem/CoralIntakeMS +``` + +Virtual subsystems use the same pattern but appear under: + +```text +/RealOutputs/LogPeriodic/VirtualSubsystem/MS +``` + +Only put robot-wide observer/coordination code in a `VirtualSubsystem`. +Motor-owning mechanisms should normally be `RBSISubsystem` classes so WPILib +command requirements work correctly. + +## Related Pages + +- [RBSI-Drive.md](RBSI-Drive.md): drivetrain and odometry signals. +- [RBSI-Vision.md](RBSI-Vision.md): vision logging and filtering. +- [RBSI-PoseBuffer.md](RBSI-PoseBuffer.md): pose timing and replay behavior. +- [RBSI-SysId.md](RBSI-SysId.md): characterization logs and mechanism tuning. diff --git a/doc/RBSI-Autonomous.md b/doc/RBSI-Autonomous.md new file mode 100644 index 00000000..3e025ab2 --- /dev/null +++ b/doc/RBSI-Autonomous.md @@ -0,0 +1,252 @@ +# Az-RBSI Autonomous Guide + +This page describes the autonomous options included in RBSI and how to choose +between Manual, PathPlanner, Choreo, and Autopilot workflows. + +Select the active autonomous stack in `Constants.java`: + +```java +private static AutoType autoType = AutoType.MANUAL; +``` + +Supported values: + +- `MANUAL`: teams write their own command sequence. +- `PATHPLANNER`: use PathPlanner and PathPlannerLib. +- `CHOREO`: use Choreo trajectories. + +Autopilot is used as a teleop drive-to-pose helper and can also inspire custom +autonomous commands. + +## Match Execution Flow + +RBSI follows the normal WPILib command-based lifecycle: + +1. `Robot` starts AdvantageKit logging and constructs `RobotContainer`. +2. `RobotContainer` constructs subsystems, configures PathPlanner or Choreo for + the selected `AutoType`, registers named commands, builds dashboard choosers, + and binds driver controls. +3. Every `robotPeriodic()` loop runs virtual subsystems first, then + `CommandScheduler.getInstance().run()`. This lets IMU and odometry samples + reach pose estimation before commands and vision consumers need them. +4. `autonomousInit()` cancels any commands left from disabled/practice, sets + drive brake mode, resets heading-controller state, opens the vision pose gate, + reads the selected autonomous command, and schedules it. +5. `teleopInit()` cancels the stored autonomous command, restores brake mode, + resets heading-controller state, and lets the default drive command take over. + +This means autonomous commands should own only their intended subsystems. A +command that requires `Drive` will interrupt the default drive command while it +runs, and teleop will cancel the stored auto before the driver command resumes. + +## Manual Autos + +`MANUAL` is the simplest mode. RBSI does not construct a PathPlanner chooser or +Choreo factory. Use this when: + +- the robot is in early bring-up, +- you are debugging drivetrain behavior, +- your team wants simple command-based autos, +- you want to avoid pathing dependencies until drive and vision are stable. + +The example `simpleAuto()` in `RobotContainer` is a placeholder for teams that +want to build autos directly with commands. + +## PathPlanner + +When `autoType` is `PATHPLANNER`, RBSI configures PathPlanner `AutoBuilder` in +`Drive` and publishes a logged dashboard chooser: + +```java +new LoggedDashboardChooser<>("Auto Choices", AutoBuilder.buildAutoChooser()) +``` + +PathPlanner depends on: + +- accurate robot pose, +- correct module translations, +- correct wheel radius, +- correct max speed, +- correct PathPlanner `RobotConfig`, +- alliance-aware path flipping. + +Important constants: + +- `AutoConstants.kPathPlannerConfig` +- `DrivebaseConstants.kMaxLinearSpeedMetersPerSec` +- `DrivebaseConstants.kWheelRadiusMeters` +- `DrivebaseConstants.kSlipCurrentAmps` +- `RobotConstants.kMass` +- `RobotConstants.kMomentOfInertiaKgMetersSq` +- `RobotConstants.kWheelCoefficientOfFriction` + +PathPlanner chooser entries also include drive and flywheel SysId routines. +This is intentional: characterization commands are easiest to run from the +same dashboard path used for autonomous selection. + +RBSI configures PathPlanner `AutoBuilder` in `Drive` with: + +- `this::getPose` +- `this::resetPose` +- `this::getChassisSpeeds` +- `(speeds, feedforwards) -> runVelocity(speeds)` +- `PPHolonomicDriveController` +- `AutoConstants.kPathPlannerConfig` +- alliance-aware path flipping +- the `Drive` subsystem requirement + +PathPlannerLib's holonomic AutoBuilder expects robot-relative measured speeds +and robot-relative output speeds. `Drive.getChassisSpeeds()` returns +robot-relative speeds from the module states, and `Drive.runVelocity(...)` +accepts robot-relative chassis speeds, so the callback pair is intentionally +not field-relative. + +## Choreo + +When `autoType` is `CHOREO`, RBSI constructs an `AutoFactory` and uses the +Choreo sample-following support in `Drive`. + +Important constants: + +- `AutoConstants.kChoreoDrivePID` +- `AutoConstants.kChoreoSteerPID` + +Choreo should be tuned after the drivetrain already tracks pose accurately. +If a Choreo path misses badly, do not start by changing PID constants. First +verify odometry, wheel radius, gyro orientation, and pose reset behavior. + +RBSI constructs the Choreo `AutoFactory` in `RobotContainer` with: + +- `m_drivebase::getPose` +- `m_drivebase::resetPose` +- `m_drivebase::followTrajectory` +- alliance flipping enabled +- `m_drivebase` as the subsystem requirement + +Each routine should reset odometry once at the start of the first trajectory. +The example routine does that with: + +```java +routine.active().onTrue(Commands.sequence(pickupTraj.resetOdometry(), pickupTraj.cmd())); +``` + +Do not also reset pose in a separate command at the same trigger unless you +intend to override the path's starting pose. + +## Autopilot + +Autopilot is configured in `AutoConstants`: + +- `kAPConstraints` +- `kAPProfile` +- `kAutopilot` + +RBSI exposes several `AutopilotCommands.runAutopilot(...)` overloads for +drive-to-pose behavior. It logs useful values under `Autopilot/*`, including: + +- current pose, +- final pose, +- robot speeds, +- output velocities, +- target angle, +- at-target state. + +Tune Autopilot after driver controls and pose estimation are stable. + +## Named Commands + +PathPlanner named commands are registered before autos and paths are created. +Keep that ordering. If a path references a named command that is not registered +before the chooser is built, the path can fail to load or run incorrectly. + +When adding mechanisms: + +1. Add subsystem code. +2. Add commands. +3. Register named commands in `RobotContainer`. +4. Reference those exact names in PathPlanner. +5. Test each named command by itself before embedding it in a full auto. + +## Pose Reset Rules + +Autonomous pathing depends on a clean starting pose. RBSI avoids fighting +PathPlanner’s `AutoBuilder` pose handling by keeping starting-pose reset logic +centralized. + +General rules: + +- Let PathPlanner reset pose for PathPlanner autos. +- Let Choreo reset pose for Choreo autos. +- For manual autos, explicitly reset pose only when the command owns the + starting condition. +- Avoid resetting pose from multiple commands at the same time. +- Use `Drive.resetPose(...)` for autonomous resets so the pose reset epoch and + vision pose gate stay aligned. + +Unexpected pose resets are one of the fastest ways to make a correct path look +wrong. + +## Running Characterization Commands + +When using PathPlanner mode, the auto chooser includes: + +- drive wheel radius characterization, +- drive feedforward characterization, +- drive SysId routines, +- flywheel SysId routines. + +Run these with the robot in a safe area and the Driver Station ready to disable. +Characterization commands intentionally command motors in ways that do not feel +like normal teleop driving. + +## Recommended Autonomous Bring-Up Order + +1. Start in `MANUAL` mode. +2. Verify robot-relative drive. +3. Verify field-relative drive. +4. Verify odometry on a straight-line push test. +5. Verify vision is not corrupting pose. +6. Characterize wheel radius and feedforward. +7. Switch to `PATHPLANNER` or `CHOREO`. +8. Run a short, slow straight path. +9. Run a slow turn path. +10. Add mechanism commands. +11. Increase speed only after repeatability is good. + +## Troubleshooting + +Auto does nothing: + +- Check `autoType`. +- Check the correct chooser is visible. +- Check the selected auto is not `Commands.none()`. +- Check named commands are registered. + +Path starts from the wrong place: + +- Check starting pose reset. +- Check alliance flipping. +- Check field layout. +- Check odometry before auto starts. + +Path tracks poorly: + +- Verify wheel radius. +- Verify max speed. +- Verify gyro orientation. +- Tune drive PID only after the physical constants are right. + +Robot follows path in mirror image: + +- Check alliance-aware flipping logic. +- Check PathPlanner field coordinate assumptions. +- Check robot heading at auto start. + +## Related Pages + +- [RBSI-Drive.md](RBSI-Drive.md): odometry, robot-relative speed conventions, + and drive characterization. +- [RBSI-Vision.md](RBSI-Vision.md): vision filtering before autonomous pose + fusion. +- [RBSI-Constants.md](RBSI-Constants.md): `AutoConstants` and drive constants + used by autonomous frameworks. diff --git a/doc/RBSI-Constants.md b/doc/RBSI-Constants.md new file mode 100644 index 00000000..c026f1bc --- /dev/null +++ b/doc/RBSI-Constants.md @@ -0,0 +1,343 @@ +# RBSI Constants Guide + +This document describes `src/main/java/frc/robot/Constants.java`, what each section controls, and +which sections a team should tune first when adapting RBSI to a new robot. + +`Constants.java` is intended to be a robot configuration surface, not a logic file. New projects +should keep robot-specific values here, keep behavior in subsystems and commands, and prefer names +that include units when the Java type does not already carry units. + +## Naming Pattern + +The constants file now follows these conventions: + +- Every public constant starts with `k`. +- Names avoid repeating the section name. For example, inside `FlywheelConstants`, use `kGearRatio` + instead of `kFlywheelGearRatio`. +- Plain `double` values include units when practical, such as `kMaxLinearSpeedMetersPerSec`, + `kSlipCurrentAmps`, and `kWheelLockTimeSecs`. +- Vendor or WPILib unit-typed values may use shorter names because their type carries the unit. +- Old compatibility aliases have intentionally been removed. This is a template, so new projects + should learn the clean names from the beginning. + +## Robot Selection + +Top-level private fields select the active robot and feature stack: + +- `robotType` +- `swerveType` +- `phoenixPro` +- `autoType` +- `visionType` + +Teams should check these before deploying. The selected `robotType` determines `REAL`, `REPLAY`, or +`SIM` mode through `getMode()`. Do not deploy with `SIMBOT`; the `main(...)` guard rejects it. + +Most teams should tune these immediately: + +- `robotType`: select the physical robot. +- `swerveType`: `PHOENIX6` or `YAGSL`. +- `phoenixPro`: match the CTRE license status. +- `autoType`: choose `MANUAL`, `PATHPLANNER`, or `CHOREO`. +- `visionType`: choose `PHOTON`, `LIMELIGHT`, or `NONE`. + +## General Constants + +General constants are robot-wide support values: + +- `kLoopPeriodSecs`: main robot loop period. +- `kTuningMode`: enables live tunable-number behavior. +- `kGravityMetersPerSecSq`: conversion from g to meters per second squared. + +Most teams should leave these alone unless they have a specific framework-level reason. + +## RobotConstants + +`RobotConstants` contains physical robot properties used by simulation, path planning, and drive +models: + +- `kMass` +- `kChassisMatter` +- `kMomentOfInertiaKgMetersSq` +- `kWheelCoefficientOfFriction` +- `kMaxWheelTorqueNm` +- `kRioOrientation` +- `kIMUOrientation` + +Tune these carefully: + +- `kMass`: measured robot weight including battery and bumpers. +- `kMomentOfInertiaKgMetersSq`: estimate from CAD or measurement. +- `kWheelCoefficientOfFriction`: carpet/wheel traction estimate. +- `kRioOrientation` and `kIMUOrientation`: must match the physical mounting orientation. + +Incorrect orientation constants can corrupt acceleration logging, heading interpretation, and any +logic that rotates sensor values into robot coordinates. + +## PowerConstants + +`PowerConstants` configures the power distribution device and current/voltage alert thresholds: + +- `kPdmType` +- `kPdmCanId` +- `kTotalMaxCurrentAmps` +- `kMotorPortMaxCurrentAmps` +- `kSmallPortMaxCurrentAmps` +- `kWarningVoltage` +- `kLimitingVoltage` +- `kCriticalVoltage` + +Tune these early for the real robot: + +- `kPdmType` and `kPdmCanId`: must match the installed PDH/PDP. +- Current limits: match breaker, wiring, and mechanism expectations. +- Voltage thresholds: tune alert levels for your team’s brownout policy. + +## CANBuses + +`CANBuses` names the robot CAN networks: + +- `RIO` +- `DRIVE` +- `ALL` + +These names must match CTRE, REV, and generated swerve configuration expectations. Add any +additional CANivore or vendor buses here and include them in `ALL` so health monitoring sees them. + +## RobotDevices + +`RobotDevices` maps mechanisms to CAN IDs, CAN bus names, and power distribution ports. + +Drivetrain IDs are sourced from `SwerveConstants`, while power ports are assigned here. Mechanism +devices, such as the example flywheel leader and follower, are also assigned here. + +Teams should tune this section carefully: + +- Verify every CAN ID. +- Verify every bus name. +- Verify every PDH/PDP port. +- Use `null` for devices that do not consume a power distribution channel directly, such as + CANcoders. + +Bad entries here cause misleading power logs and can make CAN health debugging much harder. + +## SensorConstants + +`SensorConstants` holds configuration values for robot-wide sensors that do not naturally belong to +one mechanism: + +- `kRioAccelerometerSampleRateHz` + +Most teams can leave this alone. Increase it only if you have a specific acceleration or jerk +logging need and have verified that the extra sampling work is worth it. + +## OperatorConstants + +`OperatorConstants` configures driver controls: + +- `kDriveStyle` +- `kJoystickDeadband` +- `kTurnSensitivity` +- `kJoystickSlewRateLimit` +- `kRobotRelativeNudgeSpeedMetersPerSec` +- `kAutopilotDemoXOffsetMeters` +- driver/operator switch IDs +- `MULTI_TOGGLE` + +Tune this section with drivers: + +- Set `kDriveStyle` to the preferred boot/default drive style. `TANK` uses the left stick for + translation and the right stick for turning; `GAMER` uses the right stick for translation and the + left stick for turning. RBSI also exposes this choice as the `Drive Style` dashboard chooser, so + teams can switch between drivers without recompiling or redeploying. +- Adjust `kJoystickDeadband` until joystick drift disappears without making the robot feel numb. +- Adjust `kTurnSensitivity` and `kJoystickSlewRateLimit` after real driver practice. +- Adjust `kRobotRelativeNudgeSpeedMetersPerSec` to change the POV nudge speed from a single place. +- Adjust or remove `kAutopilotDemoXOffsetMeters` when replacing the example bumper drive-to-pose + binding with a game-specific target. +- Verify all console switches against the actual HID device. + +## DrivebaseConstants + +`DrivebaseConstants` is one of the most important tuning sections. It controls physical drive +limits, drive PID constants, SysId settings, ramp rates, and odometry buffering. + +High-priority robot-specific values: + +- `kMaxLinearSpeedMetersPerSec` +- `kSlipCurrentAmps` +- `kWheelRadiusMeters` +- `kMaxLinearAccelMetersPerSecSq` +- `kStrafeP`, `kStrafeI`, `kStrafeD` +- `kSpinP`, `kSpinI`, `kSpinD` +- `kDriveP`, `kDriveD`, `kDriveV`, `kDriveA`, `kDriveS`, `kDriveT` +- `kSteerP`, `kSteerD`, `kSteerS` + +Characterize or measure these: + +- `kMaxLinearSpeedMetersPerSec`: use the real robot, not a theoretical free-speed estimate. +- `kWheelRadiusMeters`: use the wheel radius characterization routine. +- `kSlipCurrentAmps`: measure against carpet, ideally at an event carpet. +- drive feedforward gains: use drive characterization routines. + +Odometry and disabled-vision behavior: + +- `kPoseBufferHistorySecs`: history window used for latency compensation. +- `kDisabledVisionBlendAlpha`: how aggressively disabled vision pulls the pose estimate. +- `kDisabledVisionMaxJumpM` and `kDisabledVisionMaxJumpRad`: reject unreasonable disabled-vision + jumps. +- `kDisabledCoastSeconds`: time to keep disabled coast behavior active. +- `kDisabledCoastMinSeconds`: minimum time before stationary detection can end the coast phase. +- `kDisabledVisionCoastBlendAlpha`: gentler disabled-vision blend while the robot is still + coasting. +- stationary detection constants: tune if disabled pose behavior ends too quickly or too slowly. + +Do not blindly copy drive gains between robots. Module type, wheel type, gearing, mass, current +limits, and CTRE license status all matter. + +Drive characterization helpers also use constants here: + +- `kSysIdPreRunStopSecs`: short stop/settle period before WPILib SysId drive tests. +- `kFeedforwardCharacterizationStartDelaySecs`: module-orientation delay before simple drive + feedforward characterization begins collecting samples. +- `kFeedforwardCharacterizationRampRateVoltsPerSec`: voltage ramp rate for simple drive + feedforward characterization. +- `kWheelRadiusCharacterizationStartDelaySecs`: module-orientation delay before wheel radius + characterization begins measuring. +- `kWheelRadiusCharacterizationMaxVelocityRadPerSec`: maximum robot spin speed during wheel radius + characterization. +- `kWheelRadiusCharacterizationRampRateRadPerSecSq`: spin-speed slew rate during wheel radius + characterization. + +## FlywheelConstants + +`FlywheelConstants` configures the example flywheel subsystem: + +- `kIdleMode` +- `kGearRatio` +- `kMaxVoltage` +- `kClosedLoopRampPeriodSecs` +- `kOpenLoopRampPeriodSecs` +- SysId settings +- CTRE Motion Magic Velocity settings +- real feedforward and feedback gains +- sim feedforward and feedback gains +- sim plant gearing and moment of inertia + +Tune this section when using the example flywheel: + +- `kGearRatio`: must match motor rotations to mechanism rotations. +- `kMaxVoltage`: voltage clamp for simulation and mechanism safety expectations. +- `kMotionMagicAccelerationRotPerSecSq` and `kMotionMagicJerkRotPerSecCubed`: CTRE Motion Magic + Velocity profile settings for TalonFX control. +- `kRealS`, `kRealV`, `kRealA`: copy from WPILib SysId voltage characterization. +- `kRealP`, `kRealD`: tune after feedforward is correct. +- `kSimS`, `kSimV`, `kSimA`, `kSimP`, `kSimD`: tune separately for simulation. +- `kSimGearing` and `kSimMomentOfInertiaKgMetersSq`: tune when changing the example simulated + flywheel's mass, radius, gearing, or motor. + +See `doc/RBSI-SysId.md` for how to run the flywheel SysId routines and apply the results. + +## AutoConstants + +`AutoConstants` configures autonomous/pathing frameworks: + +- `kPathPlannerConfig` +- `kChoreoDrivePID` +- `kChoreoSteerPID` +- `kAutopilot` + +Tune this section after the drivetrain constants are correct. PathPlanner and Choreo depend on +accurate robot mass, moment of inertia, wheel radius, friction, current, and drive speed constants. + +Pay special attention to: + +- PathPlanner `RobotConfig`: depends on `RobotConstants`, `DrivebaseConstants`, and + `SwerveConstants`. +- `kChoreoDrivePID` and `kChoreoSteerPID`: tune for trajectory tracking. +- Autopilot constraints and tolerances: tune for teleop drive-to-pose behavior. + +## VisionConstants + +`VisionConstants` configures AprilTag trust, filtering, and measurement uncertainty: + +- `kTrustedTags` +- trusted/untrusted standard deviation scaling +- ambiguity and target logging thresholds +- field border and z-height margins +- linear/angular standard deviation baselines +- MegaTag2 standard deviation scaling + +Tune these after cameras are mounted and calibrated: + +- `kTrustedTags`: choose tags that are reliable for your game strategy and field side. +- `kMaxAmbiguity`: reject single-tag observations that are too ambiguous. +- `kMaxZErrorMeters`: reject pose estimates with physically unreasonable height. +- `kLinearStdDevBaseline` and `kAngularStdDevBaseline`: tune how much vision influences pose. +- trusted/untrusted scale factors: tune tag family confidence. + +If the robot pose jumps, first verify camera transforms in `Cameras`, then revisit these noise and +gate constants. + +## Cameras + +`Cameras` defines PhotonVision camera configurations: + +- camera name +- robot-to-camera transform +- per-camera standard deviation factor +- simulation camera properties + +This section is critical for vision accuracy: + +- `robotToCamera` must match the physical mounting location and angle. +- Rotation values are radians. +- Camera names must match the PhotonVision UI. +- Simulation calibration should roughly match the real camera. + +Limelight users should configure camera transforms in the Limelight web UI where appropriate, but +the robot code still depends on `VisionConstants` for filtering and trust rules. + +## DeployConstants + +`DeployConstants` names deploy subdirectories: + +- `kAprilTagDir` +- `kChoreoDir` +- `kPathPlannerDir` +- `kYagslDir` + +Most teams should not change these unless they intentionally reorganize deploy files. + +## Recommended Tuning Order For A New Robot + +1. Select `robotType`, `swerveType`, `autoType`, `visionType`, and `phoenixPro`. +2. Configure `CANBuses` and `RobotDevices`. +3. Set `RobotConstants` mass, moment of inertia, wheel friction, and sensor orientations. +4. Set `SensorConstants` and `PowerConstants`. +5. Tune `OperatorConstants` with driver feedback. +6. Characterize and tune `DrivebaseConstants`. +7. Configure and validate cameras in `Cameras`. +8. Tune `VisionConstants`. +9. Tune `AutoConstants` after drive and vision are stable. +10. Tune mechanism sections such as `FlywheelConstants`. +11. Re-run tests and simulation after each major tuning pass. + +## Common Failure Modes + +- Wrong CAN bus name: devices appear disconnected or health logs look empty. +- Wrong power port: current logs blame the wrong subsystem. +- Wrong IMU orientation: heading or acceleration appears rotated. +- Wrong camera transform: vision updates pull pose in the wrong direction. +- Wrong gear ratio: velocity control and SysId constants do not transfer correctly. +- Untuned max speed or wheel radius: autonomous path following misses distance targets. +- Over-trusting vision: pose jumps toward bad tag solves. +- Under-trusting vision: odometry drift is never corrected. + +## Related Pages + +- [RBSI-Drive.md](RBSI-Drive.md): drivetrain constants, Phoenix Tuner X files, + odometry, and characterization. +- [RBSI-Vision.md](RBSI-Vision.md): camera constants and pose-estimation tuning. +- [RBSI-Autonomous.md](RBSI-Autonomous.md): PathPlanner, Choreo, and Autopilot + constants in match context. diff --git a/doc/RBSI-Drive.md b/doc/RBSI-Drive.md new file mode 100644 index 00000000..3e943ddd --- /dev/null +++ b/doc/RBSI-Drive.md @@ -0,0 +1,248 @@ +# Az-RBSI Drive Subsystem Guide + +This page describes the RBSI drive subsystem, the odometry pipeline, and the +constants teams should tune when adapting the template to a new drivetrain. + +The drive code is split into two responsibilities: + +- `Drive`: command-facing subsystem that accepts chassis speeds, owns the pose + estimator, exposes robot pose, and configures autonomous builders. +- `DriveOdometry`: virtual subsystem that drains timestamped odometry queues, + updates the pose estimator, and maintains pose/yaw buffers. + +This split keeps hardware control and latency-sensitive estimator replay from +tripping over each other. + +## Supported Swerve Paths + +Choose the swerve stack in `Constants.java`: + +```java +private static SwerveType swerveType = SwerveType.PHOENIX6; +``` + +Supported values: + +- `PHOENIX6`: all-CTRE swerve generated by Phoenix Tuner X. +- `YAGSL`: mixed-vendor swerve generated by YAGSL. + +The all-CTRE path is the reference path for this template. YAGSL support exists +for teams with mixed hardware, but teams should validate it carefully on their +own robot. + +## Phoenix Tuner X Constants + +For `PHOENIX6`, RBSI expects Phoenix Tuner X output in +`src/main/java/frc/robot/generated/`. + +Phoenix Tuner X generates a file named `TunerConstants.java`. RBSI keeps one +renamed copy per robot instead: + +- `COMPBOTTunerConstants.java` +- `DEVBOT1TunerConstants.java` +- `DEVBOT2TunerConstants.java` + +After generating a real robot file, copy it into `src/main/java/frc/robot/generated/`, +rename it to the matching robot type, and update the Java class name. Then +comment out the generated `CommandSwerveDrivetrain` import and the generated +`createDrivetrain()` function. RBSI does not use CTRE's generated command +drivetrain; it uses its own `Drive`, `ModuleIOTalonFX`, odometry, logging, and +pose-estimation stack. + +The `generated` package uses a small View layer: + +- `TunerView` defines the constants RBSI needs. +- Each `*TunerView` adapts one per-robot generated constants file. +- `TunerFactory` selects the correct view from `Constants.getRobot()`. +- `SwerveConstants`, `ModuleIOTalonFX`, and `PhoenixOdometryThread` consume + `TunerFactory.INSTANCE`. + +This keeps the rest of the robot code independent of a specific generated +filename. More detailed copy steps are in `src/main/java/frc/robot/generated/README` +and [INSTALL.md](INSTALL.md). + +## Hardware Configuration + +Drivetrain CAN IDs come from generated or parsed swerve constants. Power +distribution ports are mapped in `Constants.RobotDevices`. + +Check these first: + +- drive motor CAN IDs, +- steer motor CAN IDs, +- encoder CAN IDs, +- CAN bus names, +- Pigeon/NavX configuration, +- PDH/PDP power ports. + +The power-port mapping does not control motors, but it affects current logging +and subsystem power monitoring. Bad power-port maps make electrical debugging +much harder. + +## DrivebaseConstants + +Important values in `DrivebaseConstants`: + +- `kMaxLinearSpeedMetersPerSec` +- `kMaxLinearAccelMetersPerSecSq` +- `kWheelRadiusMeters` +- `kSlipCurrentAmps` +- `kDriveP`, `kDriveD`, `kDriveS`, `kDriveV`, `kDriveA`, `kDriveT` +- `kSteerP`, `kSteerD`, `kSteerS` +- `kStrafeP`, `kStrafeI`, `kStrafeD` +- `kSpinP`, `kSpinI`, `kSpinD` +- `kPoseBufferHistorySecs` + +Do not leave these as template values for a competition robot. They describe +your physical robot and directly affect driver feel, autonomous tracking, and +pose estimation. + +## Odometry Threads + +RBSI supports high-rate odometry through vendor-specific collection paths: + +- `PhoenixOdometryThread` for Phoenix 6 devices. +- `SparkOdometryThread` for REV devices. + +Modules register timestamped position signals with these threads. Each robot +loop, `DriveOdometry` drains the queued samples and replays them through the +pose estimator with `updateWithTime(...)`. + +This means the estimator advances using the sensor timestamps, not just the +20 ms command scheduler loop. + +## VirtualSubsystem Order + +Drive estimation depends on virtual subsystem order: + +1. `Imu` refreshes yaw and acceleration inputs. +2. `DriveOdometry` drains module odometry and updates pose/yaw buffers. +3. `Vision` consumes pose history and injects time-aligned measurements. +4. Monitoring and secondary telemetry run later. + +If this order changes, vision may read stale pose history or inject +measurements against the wrong timestamp. + +## Pose Buffers + +`Drive` owns: + +- `poseBuffer` +- `yawBuffer` +- `yawRateBuffer` + +These are used by vision and other latency-sensitive consumers. The history +length is controlled by `DrivebaseConstants.kPoseBufferHistorySecs`. + +Increase the history if vision latency is high enough that observations fall +outside the buffer. Do not make it enormous without reason; stale observations +should usually be rejected rather than fused. + +## Characterization Routines + +PathPlanner chooser entries expose: + +- Drive wheel radius characterization. +- Drive simple feedforward characterization. +- Drive SysId quasistatic forward/reverse. +- Drive SysId dynamic forward/reverse. + +Recommended order: + +1. Verify motor directions and steering offsets. +2. Run wheel radius characterization. +3. Run feedforward/SysId characterization. +4. Update drive constants. +5. Tune steering and chassis PID. +6. Test autonomous paths at low speed. +7. Increase speed after tracking is stable. + +## Tuning Driver Feel + +Driver feel is shaped by: + +- `OperatorConstants.kDriveStyle` +- `OperatorConstants.kJoystickDeadband` +- `OperatorConstants.kTurnSensitivity` +- `OperatorConstants.kJoystickSlewRateLimit` +- `DrivebaseConstants.kMaxLinearSpeedMetersPerSec` +- `DrivebaseConstants.kMaxLinearAccelMetersPerSecSq` + +RBSI supports two driver stick layouts: + +- `TANK`: left stick drives the robot forward/back and left/right; right stick turns the robot. +- `GAMER`: right stick drives the robot forward/back and left/right; left stick turns the robot. + +`OperatorConstants.kDriveStyle` sets the boot/default layout. `RobotContainer` also publishes a +`Drive Style` dashboard chooser with the same options, so a team can swap between driver +preferences without recompiling or redeploying code. Change the chooser before driving, then verify +forward, strafe, and turn on blocks or at low speed before full-speed practice. + +Tune these with drivers on carpet. A robot that feels good on blocks or on a +shop floor may feel different on event carpet. + +## Simulation + +Simulation uses the selected module IO simulation and `DriveSimPhysics`. + +Simulation is useful for: + +- validating command wiring, +- exercising path builders, +- checking logging paths, +- testing pose-buffer behavior, +- debugging autonomous command selection. + +Simulation is not a substitute for measuring wheel radius, slip current, or +maximum speed on the real robot. + +## Bring-Up Checklist + +1. Generate or configure swerve constants. +2. Verify `Constants.getSwerveType()`. +3. Verify every `RobotDevices` drivetrain entry. +4. Verify IMU orientation. +5. Confirm modules steer to expected angles. +6. Confirm positive chassis X drives forward. +7. Confirm positive chassis Y drives left. +8. Confirm positive omega rotates counterclockwise. +9. Run wheel radius characterization. +10. Run feedforward/SysId characterization. +11. Tune drive and steer gains. +12. Test PathPlanner or Choreo at reduced speed. + +## Troubleshooting + +Robot drives sideways when commanded forward: + +- Check module order. +- Check module translations. +- Check drive motor inversion. +- Check gyro orientation. + +Pose rotates but translation is wrong: + +- Check wheel radius. +- Check drive encoder units. +- Check gear ratio. + +Vision makes pose worse: + +- First verify drive odometry without vision. +- Then verify camera transforms. +- Then tune vision standard deviations. + +Autonomous path is mirrored or offset: + +- Check alliance-aware path flipping. +- Check initial pose reset behavior. +- Check field layout selection. +- Check PathPlanner starting pose and AutoBuilder configuration. + +## Related Pages + +- [INSTALL.md](INSTALL.md): Phoenix Tuner X copy/rename setup steps. +- [RBSI-Constants.md](RBSI-Constants.md): drivetrain and robot-wide constants. +- [RBSI-Autonomous.md](RBSI-Autonomous.md): PathPlanner and Choreo callback + behavior during a match. +- [RBSI-PoseBuffer.md](RBSI-PoseBuffer.md): odometry/vision timing details. diff --git a/doc/RBSI-GSG.md b/doc/RBSI-GSG.md index b7f541d8..6271e4a4 100644 --- a/doc/RBSI-GSG.md +++ b/doc/RBSI-GSG.md @@ -19,12 +19,13 @@ modifications to extant RBSI code will be done to files within the for `CommandXboxController` near the top of the `RobotContainer.java` file. 2. **Robot Project Constants**: All of the configurable values for your robot - will be in the ``Constants.java`` file. This file contains the outer - ``Constants`` class with various high-level configuration variables such as - ``swerveType``, ``autoType``, ``visionType``, and whether your team has - purchased a [CTRE Pro license](https://v6.docs.ctr-electronics.com/en/stable/docs/licensing/team-licensing.html) + will be in the `Constants.java` file. This file contains the outer + `Constants` class with high-level configuration variables such as + `swerveType`, `autoType`, `visionType`, and whether your team has purchased + a [CTRE Pro license](https://v6.docs.ctr-electronics.com/en/stable/docs/licensing/team-licensing.html) for unlocking some of the more advanced communication and control features - available for CTRE devices. + available for CTRE devices. See [RBSI-Constants.md](RBSI-Constants.md) for + a field-by-field guide to the constants file. 3. **Robot Physical Constants**: The next four classes in ``Constants.java`` contain information about the robot's physical characteristics, power @@ -67,8 +68,8 @@ field.** 6. In the `Constants.java` file, the classes following `RobotDevices` contain individual containers for robot subsystems and interaction methods. The - `OperatorConstants` class determines how the OPERATOR interacts with the - robot. `DriveBaseConstants` and `FlywheelConstants` (and additional classes + `OperatorConstants` class determines how the operator interacts with the + robot. `DrivebaseConstants` and `FlywheelConstants` (and additional classes you add for your own mechanisms) contain human-scale conversions and limits for the subsystem (_e.g._, maximum speed, gear ratios, PID constants, etc.). `AutoConstants` contains the values needed for your autonomous period method @@ -96,6 +97,18 @@ Additionally, both [PhotonVision](https://docs.photonvision.org/en/latest/) and https://docs.limelightvision.io/docs/docs-limelight/getting-started/summary) computer vision systems are supported in the present release. +For deeper subsystem bring-up, use these pages after the first compile and +deploy: + +* [RBSI-Drive.md](RBSI-Drive.md): swerve configuration, odometry ordering, pose + buffers, and drive characterization. +* [RBSI-Vision.md](RBSI-Vision.md): PhotonVision and Limelight configuration, + camera transforms, observation filtering, and simulation. +* [RBSI-Autonomous.md](RBSI-Autonomous.md): Manual, PathPlanner, Choreo, and + Autopilot autonomous workflows. +* [RBSI-SysId.md](RBSI-SysId.md): example flywheel SysId routines and how to use + the generated gains. + -------- ### Included 3D Prints @@ -132,21 +145,23 @@ section of [each release](https://github.com/AZ-First/Az-RBSI/releases). See the [PhotonVision Wiring documentation ](https://docs.photonvision.org/en/latest/docs/quick-start/wiring.html) for - more details. DO NOT put the orange pi's (or any devices that cannnot loose power) on port 23 of the PDH. It is a mechanical switch, and if the robot is hit, it briefly will loose power. + more details. Do not put the Orange Pis, radio, or any device that cannot + lose power on port 23 of the REV PDH. Port 23 is switched, and a robot impact + can briefly interrupt power. Mounting the case to the robot requires 4x #10-32 nylock nuts (placed in the hex-shaped mounts inside the case) and 4x #10-32 bolts. - Order of addembly of the Orange Pi Double Case matters given tight clearances: + Order of assembly of the Orange Pi Double Case matters given tight clearances: 1. Super-glue the nylock nuts into the hex mounting holes. - 2. Intall the fans and grates into the case side. - 3. Assemble the Pi's into the standoffs outside the box. - 4. Solder / mount the Voltage Regular solution of your choice. + 2. Install the fans and grates into the case side. + 3. Assemble the Pis into the standoffs outside the box. + 4. Solder or mount the voltage regulator solution of your choice. 5. Connect the USB-C power cables to the Pi's. 6. Connect the fan power to the 5V (red) and GND (black) pins in the Pi's. 7. Install the Pi/standoff assembly into the case using screws at the bottom, be careful of the tight clearance between the USB sockets and the case opening. - 8. Tie a knot in the incoing power line _to be placed inside the box + 8. Tie a knot in the incoming power line _to be placed inside the box for strain relief_, and pass the incoming power line through the notch in the lower case. 9. Install the cover on the box using screws. diff --git a/doc/RBSI-SysId.md b/doc/RBSI-SysId.md new file mode 100644 index 00000000..434cfce2 --- /dev/null +++ b/doc/RBSI-SysId.md @@ -0,0 +1,242 @@ +# RBSI SysId Characterization + +This document describes the RBSI SysId routines for the example flywheel and how to use the +resulting data with WPILib SysId, AdvantageKit, CTRE Phoenix 6, and REVLib. + +## References + +- WPILib SysId routine setup: + https://docs.wpilib.org/en/stable/docs/software/advanced-controls/system-identification/creating-routine.html +- WPILib SysId data loading: + https://docs.wpilib.org/en/stable/docs/software/advanced-controls/system-identification/loading-data.html +- WPILib 2026 `SysIdRoutine.Config` API: + https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/wpilibj2/command/sysid/SysIdRoutine.Config.html +- AdvantageKit SysId compatibility: + https://docs.advantagekit.org/data-flow/sysid-compatibility +- CTRE Phoenix 6 SysId integration: + https://v6.docs.ctr-electronics.com/en/stable/docs/api-reference/wpilib-integration/sysid-integration/plumbing-and-running-sysid.html +- CTRE Phoenix 6 control requests: + https://v6.docs.ctr-electronics.com/en/latest/docs/migration/migration-guide/control-requests-guide.html +- REVLib Spark configuration: + https://docs.revrobotics.com/revlib/spark/configuring-a-spark +- REVLib Spark velocity control: + https://docs.revrobotics.com/revlib/spark/closed-loop/velocity-control-mode +- REVLib `SparkClosedLoopController.setSetpoint` API: + https://codedocs.revrobotics.com/java/com/revrobotics/spark/sparkclosedloopcontroller + +## Implemented Routines + +RBSI exposes two SysId routine families for the example flywheel: + +- `FlywheelVoltage`: direct voltage control. +- `FlywheelDutyCycle`: duty-cycle control, where the SysId requested voltage is converted to a + percent output using the current roboRIO battery voltage. + +The voltage routine is the primary routine for identifying feedforward constants. It uses the +vendor voltage APIs directly: + +- Phoenix 6: `VoltageOut` +- REVLib Spark: `SparkBase.setVoltage` +- Simulation: `FlywheelSim.setInputVoltage` + +The duty-cycle routine is useful as a comparison check. It exercises the duty-cycle path: + +- Phoenix 6: `DutyCycleOut` +- REVLib Spark: `SparkBase.set` +- Simulation: requested percent times `RobotController.getBatteryVoltage()` + +Use duty-cycle results as a sanity check against the direct-voltage fit. If the two fits are very +different, inspect battery sag, current limiting, ramp rates, gearing, follower configuration, and +logged applied voltage before copying constants. + +Closed-loop velocity methods are implemented separately for normal mechanism control: + +- Phoenix 6 regular velocity: `VelocityVoltage` +- Phoenix 6 profiled velocity: `MotionMagicVelocityVoltage` +- REVLib Spark velocity: `SparkClosedLoopController.setSetpoint(..., ControlType.kVelocity, ...)` + +Do not use closed-loop velocity control to generate feedforward fits in WPILib SysId. SysId is +trying to identify the plant from applied voltage, position, and velocity. A closed-loop velocity +controller changes the voltage to chase a speed target, which makes the fit difficult to interpret. +Use SysId to find feedforward constants, then use those constants in the closed-loop velocity +controllers. + +## Chooser Entries + +The routines are published in the PathPlanner auto chooser when `Constants.getAutoType()` is +`PATHPLANNER`. + +Run all four voltage routines: + +- `Flywheel SysId Voltage (Quasistatic Forward)` +- `Flywheel SysId Voltage (Quasistatic Reverse)` +- `Flywheel SysId Voltage (Dynamic Forward)` +- `Flywheel SysId Voltage (Dynamic Reverse)` + +Optional comparison routines: + +- `Flywheel SysId Duty Cycle (Quasistatic Forward)` +- `Flywheel SysId Duty Cycle (Quasistatic Reverse)` +- `Flywheel SysId Duty Cycle (Dynamic Forward)` +- `Flywheel SysId Duty Cycle (Dynamic Reverse)` + +## Pre-Run Checklist + +1. Verify the flywheel can spin safely in both directions, or temporarily skip reverse tests for a + mechanism that is not safe in reverse. +2. Put the robot in a safe open area, with the flywheel pointed away from people and loose objects. +3. Use a charged battery. +4. Confirm the example flywheel IO selected in `RobotContainer` matches the hardware being tested. +5. Confirm gear ratio and encoder units in `Constants.FlywheelConstants.kGearRatio`. +6. Confirm the follower motor is physically and logically correct. A missing or inverted follower + will poison the characterization data. +7. Disable game-piece shooting code, automatic spin-up commands, and other commands that might + require the flywheel subsystem. + +## Running The Tests + +Run the voltage family first. The order recommended by WPILib is: + +1. Quasistatic Forward +2. Quasistatic Reverse +3. Dynamic Forward +4. Dynamic Reverse + +Each routine automatically ends at the configured timeout. RBSI uses: + +- quasistatic ramp rate: `Constants.FlywheelConstants.kSysIdQuasistaticRampRateVoltsPerSec` +- dynamic step voltage: `Constants.FlywheelConstants.kSysIdDynamicStepVoltageVolts` +- timeout: `Constants.FlywheelConstants.kSysIdTimeoutSecs` + +If the flywheel reaches unsafe speed before timeout, disable immediately and lower the ramp rate, +step voltage, or timeout. + +## AdvantageKit Logging Workflow + +RBSI follows the AdvantageKit SysId workflow: + +- The SysId `Mechanism` log callback is `null`. +- Flywheel inputs are logged through the `FlywheelIOInputsAutoLogged` object. +- The SysId test state is recorded with `Logger.recordOutput("SysIdTestState", ...)`. +- RBSI also records `Flywheel/SysIdRoutine` and `Flywheel/SysIdState` for easier filtering. + +Relevant logged fields: + +- `Flywheel/PositionRad` +- `Flywheel/VelocityRadPerSec` +- `Flywheel/AppliedVolts` +- `Flywheel/SysIdRoutine` +- `Flywheel/SysIdState` +- `SysIdTestState` + +AdvantageKit logs should not be opened directly in WPILib SysId. Convert them first: + +1. Open the `.wpilog` in AdvantageScope. +2. Use `File` -> `Export Data...`. +3. Export as `WPILOG`. +4. Use `AdvantageKit Cycles` timestamps. +5. Include the relevant `Flywheel` fields and `SysIdTestState`. +6. Open the exported log in WPILib SysId. + +## Loading Data In WPILib SysId + +After opening the exported log: + +1. Drag `SysIdTestState` into the test-state slot. +2. Drag flywheel position into the position slot. +3. Drag flywheel velocity into the velocity slot. +4. Drag flywheel applied voltage into the voltage slot. +5. Set analysis type to flywheel/angular mechanism. +6. Confirm units: + - position: radians + - velocity: radians per second + - voltage: volts + +The WPILib loading documentation notes that normal `SysIdRoutine` logs are named with +`sysid-test-state-`. RBSI uses AdvantageKit's `SysIdTestState` field instead, because +that is the AdvantageKit-compatible state key. + +## Applying Results + +For the voltage routine, copy the fitted constants into `Constants.FlywheelConstants`: + +- `kRealS` +- `kRealV` +- `kRealA` + +For simulation-specific fits, update: + +- `kSimS` +- `kSimV` +- `kSimA` + +Then tune feedback: + +- CTRE Phoenix 6: `FlywheelIOTalonFX.configureGains(...)` writes `kP`, `kI`, `kD`, `kS`, `kV`, + and `kA` into `Slot0`. +- REVLib Spark: `FlywheelIOSpark` configures closed-loop gains in the Spark config object and uses + WPILib `SimpleMotorFeedforward` as an arbitrary voltage feedforward during velocity control. +- Simulation: `FlywheelIOSim` uses WPILib `PIDController` and `SimpleMotorFeedforward`. + +Do feedforward first, then tune `kP` and `kD`. Leave `kI` at zero unless there is a measured, +repeatable steady-state error that feedforward and proportional control cannot address. + +## CTRE Notes + +Phoenix 6 separates control requests by output type. RBSI uses: + +- `VoltageOut` for SysId voltage characterization. +- `DutyCycleOut` for duty-cycle comparison characterization. +- `VelocityVoltage` for normal velocity control. +- `MotionMagicVelocityVoltage` for profiled velocity control. + +The TalonFX reports rotor position and velocity in rotations. RBSI converts between motor rotations +and mechanism radians using `kGearRatio`. The velocity setpoint sent to CTRE is motor +rotations per second, so RBSI multiplies mechanism rotations per second by the gear ratio. + +If Phoenix Pro is licensed, RBSI enables FOC on supported requests. If it is not licensed, Phoenix 6 +falls back for supported FOC requests. Do not use torque-current characterization unless the robot +code and analysis workflow are intentionally changed for that model. + +## REVLib Notes + +RBSI uses the 2026 Spark configuration API: + +- configure once through a `SparkFlexConfig` +- apply with `ResetMode` and `PersistMode` +- use `SparkClosedLoopController.setSetpoint` for velocity control +- pass arbitrary feedforward in volts with `ArbFFUnits.kVoltage` + +The Spark encoder velocity is reported in RPM, so RBSI converts mechanism radians per second to +motor RPM for velocity setpoints. + +If using SPARK MAX instead of SPARK Flex, use the matching REV configuration class for that device +when adapting this example. The example currently constructs `SparkMax` objects but uses the shared +Spark configuration shape already accepted by the project build. + +## Interpreting Bad Data + +Common symptoms and likely causes: + +- Quasistatic data bends sharply near high voltage: current limit, brownout, mechanical drag, or + the flywheel hit an unsafe speed. +- Forward and reverse `kS` differ greatly: direction-dependent friction, bad follower inversion, or + a mechanism that should not be characterized in reverse. +- Dynamic data is noisy: step voltage too high, flywheel too light, loose encoder signal, or + insufficient logging frequency. +- Duty-cycle fit differs from voltage fit: battery sag or vendor duty-cycle behavior is affecting + the actual applied voltage. +- Closed-loop velocity overshoots after applying feedforward: reduce `kP`, inspect `kD`, verify the + feedforward units, and confirm the gear ratio. + +## Release Checklist + +Before treating constants as final: + +1. Run all four voltage routines on the real mechanism. +2. Export the AdvantageKit log using AdvantageKit-cycle timestamps. +3. Fit in WPILib SysId and inspect residuals. +4. Update `Constants.FlywheelConstants`. +5. Rebuild and redeploy. +6. Test `runVelocity(...)` at several RPM setpoints. +7. Only then compare the duty-cycle routine, if desired. diff --git a/doc/RBSI-Vision.md b/doc/RBSI-Vision.md index 30e3ad44..916e1cb9 100644 --- a/doc/RBSI-Vision.md +++ b/doc/RBSI-Vision.md @@ -1,160 +1,368 @@ # Az-RBSI Vision Integration -This page includes detailed steps for integrating robot vision for your -2026 REBUILT robot. - --------- - -### PhotonVision - -The preferred method for adding vision to your robot is with [PhotonVision]( -https://photonvision.org/). This community-developed open-source package -combines coprocessor-based camera control and analysis with a Java library -for consuming the processed targeting information in the robot code. - -#### Recommended Setup with Az-RBSI - -We recommend using Arducam [OV9281](https://www.amazon.com/dp/B096M5DKY6) -(black & white) and/or [OV9782](https://www.amazon.com/dp/B0CLXZ29F9) (color) -cameras for robot vision due to their Global Shutter, Low Distortion, and USB -connection. In addition to the lens delivered with the camera, supplementary -lenses may be purchased to vary the FOV available to the detector for various -robot applications, such as [Low-Distortion]( -https://www.amazon.com/dp/B07NW8VR71) or [General Purpose]( -https://www.amazon.com/dp/B096V2NP2T). - -For the coprocessor that controls the cameras and analyzes the images for -AprilTag and gamepiece detection, we recommend using one or two Orange Pi 5 -single-board computers -- although PhotonVision does support a number of -[different coprocessor options]( -https://docs.photonvision.org/en/latest/docs/quick-start/quick-install.html). -As decribed in the [Getting Started Guide](RBSI-GSG.md), we include a 3D print -for a case that can hold one or two of these computers. - -#### Setting up PhotonVision on the Coprocessor - -Download the appropriate [disk image]( -https://github.com/PhotonVision/photonvision/releases/tag/v2026.2.1) for your -coprocessor and burn it to an SD card using the [Raspberry Pi Imager]( -https://www.raspberrypi.com/software). Connect the powered-on coprocessor -to the Vivivid radio (port 2 or 3) via ethernet, or connect to a network switch connected to the radio via ethernet, and connect to the PhotonVision software -at the address ``http://photonvision.local:5800``. - -Before you connect the coprocessor to your robot, be sure to set your team -number, set the IP address to "Static" and give it the number ``10.TE.AM.11``, -where "TE.AM" is the approprate parsing of your team number into IP address, -as used by your robot radio and RoboRIO. If desired, you can also give your -coprocessor a hostname. +This page describes how vision is wired into Az-RBSI for the 2026 REBUILT +robot template. It covers PhotonVision, Limelight, camera constants, +AdvantageKit logging, simulation, and the tuning steps that usually matter on a +real robot. -![PhotonVision Network Settings](PV_Network.png) +Vision in RBSI is not just a camera reader. It is part of the pose-estimation +pipeline: -We suggest you give your first coprocessor the static IP address -``10.TE.AM.11``, and your second coprocessor (if desired) ``10.TE.AM.12``. -The static address allows for more stable operation, and the these particular -addresses do not conflict with other devices on your robot network. +1. `Imu` refreshes gyro and acceleration inputs. +2. `DriveOdometry` drains timestamped module odometry and updates the pose + buffers. +3. `Vision` reads camera observations, filters them, time-aligns accepted + poses, and passes measurements to `Drive.addVisionMeasurement(...)`. -Plug in cameras (two or three per coprocessor) and navigate to the Camera -Configs page (see below). Activate the cameras. +That order matters. Vision observations are delayed by exposure time, pipeline +time, and network transport, so RBSI uses the drive pose buffer instead of +trying to correct the current robot pose with a stale camera pose. -![PhotonVision Camera Configs](PV_Cameras.png) +## Selecting A Vision Backend -#### Configuring and Calibrating your Cameras +Choose the active vision backend in `Constants.java`: -This is the most important part! +```java +private static VisionType visionType = VisionType.PHOTON; +``` -Instructions are in the [PhotonVision Documentation]( -https://docs.photonvision.org/en/latest/docs/calibration/calibration.html). +Supported values: -You should consider calibrating your cameras early and often, including daily -during a competition to ensure that the cameras are reporting as accurate a -pose as possible for your odometry. Also, double-check your calibration by -using a measuring tape to compare the reported vision-derived distance from -each camera to one or more AprilTags with reality. +- `PHOTON`: use PhotonVision cameras described by `Constants.Cameras`. +- `LIMELIGHT`: use Limelight cameras with transforms configured in Limelight. +- `NONE`: construct no cameras, useful for bring-up or robots without vision. +Real and simulation modes build camera IO differently: -#### Using PhotonVision for Vision Simulation +- Real PhotonVision: `VisionIOPhotonVision` +- Sim PhotonVision: `VisionIOPhotonVisionSim` +- Real Limelight: `VisionIOLimelight` +- Replay/no-camera shim: empty or no-op `VisionIO` -This is an advanced topic, and is therefore in the Restricted Section. (More -information about vision simulation to come in a future release.) +Use one backend at a time unless you intentionally extend the factories in +`RobotContainer`. -![Restricted Section](restricted_section.jpg) -# Az-RBSI Vision Integration +## Recommended PhotonVision Hardware + +The preferred method for adding vision to your robot is with +[PhotonVision](https://photonvision.org/). PhotonVision combines +coprocessor-based camera control and analysis with PhotonLib for consuming +processed targeting information in robot code. + +Recommended cameras: + +- Arducam [OV9281](https://www.amazon.com/dp/B096M5DKY6), black and white, + global shutter. +- Arducam [OV9782](https://www.amazon.com/dp/B0CLXZ29F9), color, global + shutter. + +Useful lens options: + +- [Low-Distortion lens](https://www.amazon.com/dp/B07NW8VR71) +- [General Purpose lens](https://www.amazon.com/dp/B096V2NP2T) -This page includes detailed steps for integrating robot vision for your -2026 REBUILT robot. +Recommended coprocessor: --------- +- One or two Orange Pi 5 single-board computers. +- Two or three cameras per coprocessor is a reasonable starting point. +- Use stable power. Do not put coprocessors on switched power ports. -### PhotonVision +PhotonVision supports other coprocessors; see the PhotonVision quick install +documentation for platform-specific images. -The preferred method for adding vision to your robot is with [PhotonVision]( -https://photonvision.org/). This community-developed open-source package -combines coprocessor-based camera control and analysis with a Java library -for consuming the processed targeting information in the robot code. +## PhotonVision Network Setup -#### Recommended Setup with Az-RBSI +Download the appropriate PhotonVision disk image for your coprocessor and burn +it to an SD card using Raspberry Pi Imager or a similar imaging tool. Connect +the powered-on coprocessor to the Vivid Hosting radio, or to a network switch +connected to the radio. -We recommend using Arducam [OV9281](https://www.amazon.com/dp/B096M5DKY6) -(black & white) and/or [OV9782](https://www.amazon.com/dp/B0CLXZ29F9) (color) -cameras for robot vision due to their Global Shutter, Low Distortion, and USB -connection. In addition to the lens delivered with the camera, supplementary -lenses may be purchased to vary the FOV available to the detector for various -robot applications, such as [Low-Distortion]( -https://www.amazon.com/dp/B07NW8VR71) or [General Purpose]( -https://www.amazon.com/dp/B096V2NP2T). +Open PhotonVision at: -For the coprocessor that controls the cameras and analyzes the images for -AprilTag and gamepiece detection, we recommend using one or two Orange Pi 5 -single-board computers -- although PhotonVision does support a number of -[different coprocessor options]( -https://docs.photonvision.org/en/latest/docs/quick-start/quick-install.html). -As decribed in the [Getting Started Guide](RBSI-GSG.md), we include a 3D print -for a case that can hold one or two of these computers. +```text +http://photonvision.local:5800 +``` -#### Setting up PhotonVision on the Coprocessor +Before the coprocessor is permanently installed: -Download the appropriate [disk image]( -https://github.com/PhotonVision/photonvision/releases/tag/v2026.2.1) for your -coprocessor and burn it to an SD card using the [Raspberry Pi Imager]( -https://www.raspberrypi.com/software). Connect the powered-on coprocessor -to the Vivivid radio (port 2 or 3) via ethernet, or connect to a network switch connected to the radio via ethernet, and connect to the PhotonVision software -at the address ``http://photonvision.local:5800``. +1. Set the team number. +2. Set the IP mode to static. +3. Use `10.TE.AM.11` for the first coprocessor. +4. Use `10.TE.AM.12` for the second coprocessor. +5. Give each coprocessor a clear hostname if desired. -Before you connect the coprocessor to your robot, be sure to set your team -number, set the IP address to "Static" and give it the number ``10.TE.AM.11``, -where "TE.AM" is the approprate parsing of your team number into IP address, -as used by your robot radio and RoboRIO. If desired, you can also give your -coprocessor a hostname. +These addresses avoid common robot-network conflicts and make camera bring-up +less mysterious during events. ![PhotonVision Network Settings](PV_Network.png) -We suggest you give your first coprocessor the static IP address -``10.TE.AM.11``, and your second coprocessor (if desired) ``10.TE.AM.12``. -The static address allows for more stable operation, and the these particular -addresses do not conflict with other devices on your robot network. +## PhotonVision Camera Setup -Plug in cameras (two or three per coprocessor) and navigate to the Camera -Configs page (see below). Activate the cameras. +Plug in cameras and open the Camera Configs page. ![PhotonVision Camera Configs](PV_Cameras.png) -#### Configuring and Calibrating your Cameras +For each camera: + +1. Activate the camera. +2. Set the camera name to exactly match `Constants.Cameras`. +3. Select the intended AprilTag pipeline. +4. Tune exposure and gain for the field lighting. +5. Confirm tags are detected at realistic match distances. +6. Calibrate the camera. + +Camera names are string keys. If PhotonVision calls a camera `Photon_BW7`, the +matching RBSI `CameraConfig` must also be named `Photon_BW7`. + +## Camera Calibration + +Camera calibration is the most important part of vision bring-up. + +Use the PhotonVision calibration documentation: + +https://docs.photonvision.org/en/latest/docs/calibration/calibration.html + +Practical rules: + +- Calibrate each physical camera. +- Recalibrate after changing a lens, focus, resolution, or mount. +- Validate calibration with a tape measure. +- Recheck calibration at events when lighting or camera exposure changes. +- Keep calibration files with the coprocessor image or team deployment notes. + +To sanity-check calibration, place the robot at known distances from several +AprilTags and compare PhotonVision’s reported camera-to-target distance against +real measurements. + +## Camera Mounting Constants + +PhotonVision camera transforms live in `Constants.Cameras`: + +```java +new CameraConfig( + "Photon_BW7", + new Transform3d( + Inches.of(-13.0), + Inches.of(13.0), + Inches.of(12.0), + new Rotation3d(0.0, 0.0, Math.PI / 2)), + 1.0, + new SimCameraProperties() { ... }) +``` + +Each camera has: + +- `name`: must match the PhotonVision camera name. +- `robotToCamera`: camera pose relative to robot center. +- `stdDevFactor`: per-camera trust multiplier. +- `simProps`: simulation calibration and latency model. + +The transform is from robot coordinates to camera coordinates: + +- X: forward positive. +- Y: left positive. +- Z: up positive. +- Rotation values are radians. + +Measure camera position from the robot center, not from the bumper edge. +Document the measurement convention in your team CAD or electrical notes. + +## Limelight Setup + +RBSI also supports Limelight through `VisionIOLimelight`. + +For Limelight: + +1. Set `visionType` to `VisionType.LIMELIGHT`. +2. Configure camera name and network identity in Limelight. +3. Configure robot-to-camera transform in the Limelight web UI. +4. Confirm MegaTag outputs are available in NetworkTables. +5. Confirm the Limelight clock and latency values are reasonable. + +RBSI consumes both MegaTag 1 and MegaTag 2 style observations when available. +MegaTag 2 observations receive special standard-deviation handling because +they generally do not provide useful robot rotation data in the same way as a +full 3D solve. + +## VisionConstants + +Vision filtering and trust are tuned in `Constants.VisionConstants`. + +Important constants: + +- `kTrustedTags`: tag IDs treated as more reliable. +- `kTrustedTagStdDevScale`: lower values make trusted tags more influential. +- `kUntrustedTagStdDevScale`: higher values make untrusted tags less + influential. +- `kRequireTrustedTag`: rejects observations that contain no trusted tags. +- `kMaxAmbiguity`: rejects ambiguous single-tag solves. +- `kFieldBorderMargin`: rejects poses outside the field plus margin. +- `kZMargin` and `kMaxZErrorMeters`: reject physically unreasonable heights. +- `kLinearStdDevBaseline`: base translation uncertainty. +- `kAngularStdDevBaseline`: base rotation uncertainty. +- `kLinearStdDevMegatag2Factor`: translation scaling for MegaTag 2. +- `kAngularStdDevMegatag2Factor`: rotation scaling for MegaTag 2. + +Start conservative. A pose estimate that is slightly slow to converge is much +easier to diagnose than a robot pose that jumps across the field because bad +vision was trusted too much. + +## Observation Filtering + +The `Vision` subsystem applies several gates before accepting an observation: + +- Timestamp must be newer than the last accepted observation for that camera. +- Timestamp must not be older than the last drive pose reset. +- Observation must contain at least one tag. +- Single-tag ambiguity must be below `kMaxAmbiguity`. +- Estimated Z must be sane. +- Estimated X/Y must be inside field bounds plus margin. +- Single-tag observations are rejected while yaw rate is too high. + +Accepted observations are scored and converted into `TimedPose` objects with +measurement standard deviations. RBSI chooses one best observation per camera, +then fuses accepted camera observations for the loop. + +## Time Alignment And Fusion + +RBSI does not simply average camera poses. + +Each accepted camera observation has its own timestamp. The `Vision` subsystem: + +1. Picks a fusion time, usually the newest accepted timestamp. +2. Uses the drive pose buffer to compute how the robot moved between each + camera timestamp and the fusion timestamp. +3. Transforms older camera poses forward to the fusion time. +4. Smooths/fuses the aligned estimates. +5. Sends a `TimedPose` to `Drive.addVisionMeasurement(...)`. + +This is why `DriveOdometry` must run before `Vision`. Vision needs a current, +coherent pose history before it can safely align delayed measurements. + +See [RBSI-PoseBuffer.md](RBSI-PoseBuffer.md) for the deeper estimator design. + +## Enabled Vs Disabled Behavior + +When enabled, RBSI uses normal WPILib pose-estimator vision fusion. The +measurement standard deviations decide how strongly the estimator trusts the +vision measurement compared with odometry. + +When disabled, RBSI uses a controlled blending path instead of repeated Kalman +updates. This prevents the estimator from behaving badly while the robot is +stationary and covariance is collapsing. The robot pose will gently walk toward +accepted vision rather than snapping violently. + +Relevant disabled-vision constants are in `DrivebaseConstants`: + +- `kDisabledVisionBlendAlpha` +- `kDisabledVisionMaxJumpM` +- `kDisabledVisionMaxJumpRad` +- `kDisabledVisionStale` +- `kDisabledVisionIgnoreAfterDisableSec` + +## AdvantageKit Logging + +Useful logged outputs: + +- `Vision/RobotToCamera*` +- `Vision/Camera*/ObsSeen` +- `Vision/Camera*/ObsAccepted` +- `Vision/Camera*/ObsRejected` +- `Vision/Camera*/RejectReason` +- `Vision/Debug/totalObsThisLoop` +- `Vision/PoseGateResetFromDrive` +- `Vision/TagIDs` +- camera inputs under `Vision/Camera*` + +If vision appears broken, open AdvantageScope and inspect: + +1. Are camera inputs updating? +2. Are tags being seen? +3. Are observations rejected? +4. What reject reason dominates? +5. Are timestamps plausible? +6. Does the camera transform look correct? + +## Simulation + +PhotonVision simulation uses: + +- `VisionSystemSim` +- `PhotonCameraSim` +- `VisionIOPhotonVisionSim` +- `SimCameraProperties` from `Constants.Cameras` + +Simulation is useful for verifying: + +- camera names, +- camera transforms, +- approximate field of view, +- pose fusion plumbing, +- AdvantageKit logging paths. + +It is not a substitute for real calibration. Simulated cameras do not capture +all real-world effects: glare, motion blur, focus, exposure, network jitter, +dirty lenses, and event lighting. + +## Bring-Up Checklist -This is the most important part! +1. Select `visionType`. +2. Configure camera names and transforms. +3. Install and image coprocessors. +4. Set static coprocessor IP addresses. +5. Calibrate every camera. +6. Confirm PhotonVision or Limelight sees tags. +7. Deploy robot code. +8. Confirm AdvantageKit camera inputs update. +9. Check observation reject reasons. +10. Tune trust and standard-deviation constants. +11. Test disabled pose convergence. +12. Test enabled driving while vision is active. +13. Test autonomous path tracking with vision enabled. -Instructions are in the [PhotonVision Documentation]( -https://docs.photonvision.org/en/latest/docs/calibration/calibration.html). +## Troubleshooting -You should consider calibrating your cameras early and often, including daily -during a competition to ensure that the cameras are reporting as accurate a -pose as possible for your odometry. Also, double-check your calibration by -using a measuring tape to compare the reported vision-derived distance from -each camera to one or more AprilTags with reality. +Camera never connects: +- Check camera name. +- Check coprocessor IP address. +- Check robot radio/switch wiring. +- Check USB cable and camera enumeration. -#### Using PhotonVision for vision simulation +Tags are seen but all poses are rejected: + +- Check `RejectReason`. +- Check tag ambiguity. +- Check field layout selection. +- Check camera calibration. +- Check robot-to-camera transform. -This is an advanced topic, and is therefore in the Restricted Section. (More -information about vision simulation to come in a future release.) +Pose jumps sideways or rotates incorrectly: -![Restricted Section](restricted_section.jpg) +- Re-measure `robotToCamera`. +- Check radians vs degrees. +- Check camera coordinate orientation. +- Check IMU orientation constants. + +Vision works disabled but gets rejected while driving: + +- Check yaw-rate gating. +- Check timestamp latency. +- Check pose buffer history length. +- Check camera exposure and blur. + +Autonomous gets worse with vision enabled: + +- Reduce trust by increasing standard deviations. +- Require trusted tags. +- Verify field layout. +- Verify all camera transforms. +- Confirm odometry is already stable without vision. + +## Related Pages + +- [RBSI-Drive.md](RBSI-Drive.md): odometry bring-up before vision fusion. +- [RBSI-PoseBuffer.md](RBSI-PoseBuffer.md): latency compensation and + time-aligned measurement design. +- [RBSI-Autonomous.md](RBSI-Autonomous.md): autonomous pose reset and match-flow + rules. diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..c4a88277 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,68 @@ +# Az-RBSI Documentation + +This directory contains the project-level documentation for the Az-RBSI robot +code template. + +Use this page as the documentation map. The root [README](../README.md) is the +project front door; this page is the working table of contents. + +## Table Of Contents + +| Page | Use It For | +| --- | --- | +| [INSTALL.md](INSTALL.md) | Creating a project, setting team number, generating Phoenix Tuner X constants, and first setup tasks. | +| [RBSI-GSG.md](RBSI-GSG.md) | First robot-code changes after installation. | +| [RBSI-Constants.md](RBSI-Constants.md) | Understanding `Constants.java` and the tuning order for a new robot. | +| [RBSI-Drive.md](RBSI-Drive.md) | Phoenix/YAGSL drivetrain setup, odometry, drive tuning, and characterization. | +| [RBSI-Vision.md](RBSI-Vision.md) | PhotonVision/Limelight setup, camera transforms, filtering, simulation, and troubleshooting. | +| [RBSI-Autonomous.md](RBSI-Autonomous.md) | Manual autos, PathPlanner, Choreo, Autopilot, match lifecycle, and pose reset rules. | +| [RBSI-SysId.md](RBSI-SysId.md) | Running SysId routines and applying flywheel characterization results. | +| [RBSI-PoseBuffer.md](RBSI-PoseBuffer.md) | Design reference for time-aligned odometry and vision fusion. | +| [RBSI-AdvantageScope.md](RBSI-AdvantageScope.md) | Standard AdvantageScope layout tabs and how to add mechanism power/timing plots. | + +## New User Bring-Up Path + +1. [Install the project](INSTALL.md). +2. [Make first code changes](RBSI-GSG.md). +3. [Select robot, swerve, vision, and auto modes](RBSI-Constants.md). +4. [Generate or configure drivetrain constants](RBSI-Drive.md). +5. [Verify odometry before vision](RBSI-Drive.md#bring-up-checklist). +6. [Configure cameras and vision filtering](RBSI-Vision.md). +7. [Bring up a short autonomous path](RBSI-Autonomous.md). +8. [Open the standard AdvantageScope layout](RBSI-AdvantageScope.md). +9. [Run SysId for mechanisms and update gains](RBSI-SysId.md). + +## Experienced User Navigation + +- Phoenix Tuner X copy/rename rules: + [INSTALL.md](INSTALL.md) and + [`src/main/java/frc/robot/generated/README`](../src/main/java/frc/robot/generated/README) +- Constants tuning order: + [RBSI-Constants.md](RBSI-Constants.md#recommended-tuning-order-for-a-new-robot) +- Drive/odometry symptoms: + [RBSI-Drive.md](RBSI-Drive.md#troubleshooting) +- Vision pose jumps: + [RBSI-Vision.md](RBSI-Vision.md#troubleshooting) +- PathPlanner/Choreo startup and reset behavior: + [RBSI-Autonomous.md](RBSI-Autonomous.md#match-execution-flow) +- Pose buffer internals: + [RBSI-PoseBuffer.md](RBSI-PoseBuffer.md) +- AdvantageScope pit-debug layout: + [RBSI-AdvantageScope.md](RBSI-AdvantageScope.md) + +## Recommended Reading Order + +For a new team: + +1. `INSTALL.md` +2. `RBSI-GSG.md` +3. `RBSI-Constants.md` +4. `RBSI-Drive.md` +5. `RBSI-Vision.md` +6. `RBSI-Autonomous.md` +7. `RBSI-SysId.md` +8. `RBSI-AdvantageScope.md` +9. `RBSI-PoseBuffer.md` + +For troubleshooting during the season, start with the symptom-specific page, +then follow links back to constants and bring-up checklists. diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java index 1f552a66..61c70354 100644 --- a/src/main/java/frc/robot/Constants.java +++ b/src/main/java/frc/robot/Constants.java @@ -63,14 +63,14 @@ public final class Constants { /***************************************************************************/ /** - * Define the various multiple robots that use this same code (e.g., COMPBOT, DEVBOT, SIMBOT, + * Define the various multiple robots that use this same code (e.g., COMPBOT, DEVBOT1, SIMBOT, * etc.) and the operating modes of the code (REAL, SIM, or REPLAY) */ private static RobotType robotType = RobotType.COMPBOT; // Define swerve, auto, and vision types being used // NOTE: Only PHOENIX6 swerve base has been tested at this point!!! - // If you have a swerve base with non-CTRE compoments, use YAGSL + // If you have a swerve base with non-CTRE components, use YAGSL // under strict caveat emptor -- and submit any error and bugfixes // via GitHub issues. private static SwerveType swerveType = SwerveType.PHOENIX6; // PHOENIX6, YAGSL @@ -80,7 +80,8 @@ public final class Constants { /** Enumerate the robot types (name your robots here) */ public static enum RobotType { - DEVBOT, // Development / Alpha / Practice Bot + DEVBOT1, // Development / Alpha / Practice Bot + DEVBOT2, // Development / Alpha / Practice Bot COMPBOT, // Competition robot SIMBOT // Simulated robot } @@ -104,44 +105,44 @@ public static void disableHAL() { /* The remainder of this file contains physical and/or software constants for the various subsystems of the robot */ /** General Constants **************************************************** */ - public static final double loopPeriodSecs = 0.02; + public static final double kLoopPeriodSecs = 0.02; - public static final boolean tuningMode = false; + public static final boolean kTuningMode = false; - public static final double G_TO_MPS2 = 9.80665; // Gravitational acceleration in m/s/s + public static final double kGravityMetersPerSecSq = 9.80665; /************************************************************************* */ /** Physical Constants for Robot Operation ******************************* */ public static final class RobotConstants { - public static final Mass kRobotMass = Pounds.of(100.); - public static final Matter kChassis = - new Matter(new Translation3d(0, 0, Inches.of(8).in(Meters)), kRobotMass.in(Kilograms)); - // Robot moment of intertial; this can be obtained from a CAD model of your drivetrain. Usually, + public static final Mass kMass = Pounds.of(100.); + public static final Matter kChassisMatter = + new Matter(new Translation3d(0, 0, Inches.of(8).in(Meters)), kMass.in(Kilograms)); + // Robot moment of inertia; this can be obtained from a CAD model of your drivetrain. Usually, // this is between 3 and 8 kg*m^2. - public static final double kRobotMOI = 6.8; + public static final double kMomentOfInertiaKgMetersSq = 6.8; // Wheel coefficient of friction - public static final double kWheelCOF = 1.2; + public static final double kWheelCoefficientOfFriction = 1.2; // Maximum torque applied by wheel // Kraken X60 stall torque ~7.09 Nm; MK4i L3 gear ratio 6.12:1 - public static final double kMaxWheelTorque = 43.4; // Nm + public static final double kMaxWheelTorqueNm = 43.4; // Insert here the orientation (CCW == +) of the Rio and IMU from the robot // An angle of "0." means the x-y-z markings on the device match the robot's intrinsic reference // frame. public static final Rotation3d kRioOrientation = switch (getRobot()) { - case COMPBOT -> new Rotation3d(0, 0, -90); - case DEVBOT -> Rotation3d.kZero; + case COMPBOT -> new Rotation3d(0, 0, Units.degreesToRadians(-90)); + case DEVBOT1, DEVBOT2 -> Rotation3d.kZero; default -> Rotation3d.kZero; }; // IMU can be one of Pigeon2 or NavX public static final Rotation3d kIMUOrientation = switch (getRobot()) { case COMPBOT -> Rotation3d.kZero; - case DEVBOT -> Rotation3d.kZero; + case DEVBOT1, DEVBOT2 -> Rotation3d.kZero; default -> Rotation3d.kZero; }; } @@ -151,18 +152,18 @@ public static final class RobotConstants { public static final class PowerConstants { // Power Distribution Module Configuration - public static final PowerDistribution.ModuleType kPDMType = PowerDistribution.ModuleType.kRev; - public static final int kPDMCANid = 1; + public static final PowerDistribution.ModuleType kPdmType = PowerDistribution.ModuleType.kRev; + public static final int kPdmCanId = 1; // Current Limits - public static final double kTotalMaxCurrent = 120.; - public static final double kMotorPortMaxCurrent = 40.; - public static final double kSmallPortMaxCurrent = 20.; + public static final double kTotalMaxCurrentAmps = 120.; + public static final double kMotorPortMaxCurrentAmps = 40.; + public static final double kSmallPortMaxCurrentAmps = 20.; // Brownout voltage levels - public static final double kVoltageWarning = 7.5; - public static final double kVoltageLimiting = 7.0; - public static final double kVoltageCritical = 6.5; + public static final double kWarningVoltage = 7.5; + public static final double kLimitingVoltage = 7.0; + public static final double kCriticalVoltage = 6.5; } /************************************************************************* */ @@ -205,7 +206,7 @@ public static class RobotDevices { new RobotDeviceId(SwerveConstants.kBLEncoderId, SwerveConstants.kBLEncoderCanbus, null); // Back Right public static final RobotDeviceId BR_DRIVE = - new RobotDeviceId(SwerveConstants.kBRDriveMotorId, SwerveConstants.kBRSteerCanbus, 2); + new RobotDeviceId(SwerveConstants.kBRDriveMotorId, SwerveConstants.kBRDriveCanbus, 2); public static final RobotDeviceId BR_ROTATION = new RobotDeviceId(SwerveConstants.kBRSteerMotorId, SwerveConstants.kBRSteerCanbus, 3); public static final RobotDeviceId BR_CANCODER = @@ -232,6 +233,14 @@ public static class RobotDevices { // public static final int INTAKE_SERVO = 0; } + /************************************************************************* */ + /** Sensor Constants ***************************************************** */ + public static final class SensorConstants { + + // RoboRIO accelerometer sampler rate. + public static final double kRioAccelerometerSampleRateHz = 200.0; + } + /************************************************************************* */ /** Operator Constants *************************************************** */ public static class OperatorConstants { @@ -239,15 +248,21 @@ public static class OperatorConstants { // Joystick Functions // Set to TANK for Drive = Left Stick, Turn = Right Stick; // Set to GAMER for Drive = Right Stick, Turn = Left Stick; - // NOTE: Intrepid programmers can turn this into a Dashboard-settable value + // RobotContainer publishes this as the default value for the Drive Style dashboard chooser. public static final DriveStyle kDriveStyle = DriveStyle.TANK; // TANK, GAMER // Joystick Deadbands - public static final double kDeadband = 0.1; - public static final double kTurnConstant = 6; + public static final double kJoystickDeadband = 0.1; + public static final double kTurnSensitivity = 6; // Joystick slew rate limiters to smooth erratic joystick motions, measured in units per second - public static final double kJoystickSlewLimit = 0.5; + public static final double kJoystickSlewRateLimit = 0.5; + + // Fixed robot-relative nudge speed used by the driver POV bindings. + public static final double kRobotRelativeNudgeSpeedMetersPerSec = Inches.of(11.0).in(Meters); + + // Demo teleop drive-to-pose target offset from the current pose. + public static final double kAutopilotDemoXOffsetMeters = Feet.of(-10.0).in(Meters); // Override and Console Toggle Switches // Assumes this controller: https://www.amazon.com/gp/product/B00UUROWWK @@ -273,46 +288,52 @@ public static final class DrivebaseConstants { // Theoretical free speed (m/s) at 12v applied output; // IMPORTANT: Follow the AdvantageKit instructions for measuring the ACTUAL maximum linear speed // of YOUR ROBOT, and replace the estimate here with your measured value! - public static final double kMaxLinearSpeed = Feet.of(18).in(Meters); + public static final double kMaxLinearSpeedMetersPerSec = Feet.of(18).in(Meters); // Slip Current -- the current draw when the wheels start to slip // Measure this against a wall. CHECK WITH THE CARPET AT AN ACTUAL EVENT!!! - public static final double kSlipCurrent = 20.0; // Amps + public static final double kSlipCurrentAmps = 20.0; // Characterized Wheel Radius (using the "Drive Wheel Radius Characterization" auto routine) public static final double kWheelRadiusMeters = Inches.of(2.000).in(Meters); - // Maximum chassis accelerations desired for robot motion -- metric / radians - // TODO: Compute the maximum linear acceleration given the PHYSICS of the ROBOT! - public static final double kMaxLinearAccel = 4.0; // m/s/s + // Maximum chassis accelerations desired for robot motion -- metric / radians. + // Estimate from drivetrain characterization and robot physics, then tune on carpet. + public static final double kMaxLinearAccelMetersPerSecSq = 4.0; // For Profiled PID Motion -- NEED TUNING! // Used in a variety of contexts, including PathPlanner and AutoPilot // Chassis (not module) across-the-field strafing motion - public static final double kPStrafe = 5.0; - public static final double kIStrafe = 0.0; - public static final double kDStrafe = 0.0; + public static final double kStrafeP = 5.0; + public static final double kStrafeI = 0.0; + public static final double kStrafeD = 0.0; // Chassis (not module) solid-body rotation - public static final double kPSPin = 5.0; - public static final double kISPin = 0.0; - public static final double kDSpin = 0.0; + public static final double kSpinP = 5.0; + public static final double kSpinI = 0.0; + public static final double kSpinD = 0.0; // Hold time on motor brakes when disabled - public static final double kWheelLockTime = 10; // seconds + public static final double kWheelLockTimeSecs = 10; // SysID characterization constants - public static final double kMaxV = 12.0; // Max volts - public static final double kDelay = 3.0; // seconds - public static final double kQuasiTimeout = 5.0; // seconds - public static final double kDynamicTimeout = 3.0; // seconds + public static final double kSysIdMaxVoltage = 12.0; + public static final double kSysIdDelaySecs = 3.0; + public static final double kSysIdQuasistaticTimeoutSecs = 5.0; + public static final double kSysIdDynamicTimeoutSecs = 3.0; + public static final double kSysIdPreRunStopSecs = 1.0; + public static final double kFeedforwardCharacterizationStartDelaySecs = 2.0; + public static final double kFeedforwardCharacterizationRampRateVoltsPerSec = 0.1; + public static final double kWheelRadiusCharacterizationStartDelaySecs = 1.0; + public static final double kWheelRadiusCharacterizationMaxVelocityRadPerSec = 0.25; + public static final double kWheelRadiusCharacterizationRampRateRadPerSecSq = 0.05; // Drive motor open-loop and closed-loop ramp periods for current smoothing // Time from from 0 -> full duty - public static final double kDriveClosedLoopRampPeriod = 0.15; // seconds - public static final double kDriveOpenLoopRampPeriod = 0.25; // seconds + public static final double kDriveClosedLoopRampPeriodSecs = 0.15; + public static final double kDriveOpenLoopRampPeriodSecs = 0.25; - public static final double kOptimalVoltage = 12.0; // Volts - public static final double kNominalFFVolts = 2.0; // Volts + public static final double kNominalVoltage = 12.0; + public static final double kNominalFeedforwardVolts = 2.0; // Default TalonFX Gains (Replaces what's in Phoenix X's Tuner Constants) // NOTE: Default values from 6328's 2025 Public Code @@ -331,7 +352,7 @@ public static final class DrivebaseConstants { public static final double kSteerS = 2.0; // Odometry-related constants ================================== - public static final double kHistorySize = 1.5; // seconds + public static final double kPoseBufferHistorySecs = 1.5; // How aggressively to pull pose toward vision while DISABLED. // 0.10 = gentle, 0.25 = fairly quick, 1.0 = full snap. public static final double kDisabledVisionBlendAlpha = 0.15; @@ -342,6 +363,8 @@ public static final class DrivebaseConstants { // Coast window config public static final double kDisabledCoastSeconds = 5.0; + public static final double kDisabledCoastMinSeconds = 0.25; + public static final double kDisabledVisionCoastBlendAlpha = 0.05; // "Stationary" detection config (tune) public static final double kStationaryMaxWheelDeltaM = 0.002; // 2mm per loop @@ -355,33 +378,47 @@ public static final class DrivebaseConstants { public static final class FlywheelConstants { // Mechanism idle mode - public static final MotorIdleMode kFlywheelIdleMode = MotorIdleMode.COAST; // BRAKE, COAST + public static final MotorIdleMode kIdleMode = MotorIdleMode.COAST; // BRAKE, COAST // Mechanism motor gear ratio - public static final double kFlywheelGearRatio = 1.5; + public static final double kGearRatio = 1.5; + public static final double kMaxVoltage = 12.0; // Flywheel motor open-loop and closed-loop ramp periods for current smoothing // Time from from 0 -> full duty - public static final double kFlywheelClosedLoopRampPeriod = 0.15; // seconds - public static final double kFlywheelOpenLoopRampPeriod = 0.25; // seconds + public static final double kClosedLoopRampPeriodSecs = 0.15; + public static final double kOpenLoopRampPeriodSecs = 0.25; + + // SysId characterization settings + public static final double kSysIdQuasistaticRampRateVoltsPerSec = 1.0; + public static final double kSysIdDynamicStepVoltageVolts = 7.0; + public static final double kSysIdTimeoutSecs = 10.0; + + // CTRE Motion Magic Velocity settings + public static final double kMotionMagicAccelerationRotPerSecSq = 400.0; + public static final double kMotionMagicJerkRotPerSecCubed = 4000.0; // MODE == REAL / REPLAY // Feedforward constants - public static final double kSreal = 0.1; - public static final double kVreal = 0.05; - public static final double kAreal = 0.0; + public static final double kRealS = 0.1; + public static final double kRealV = 0.05; + public static final double kRealA = 0.0; // Feedback (PID) constants - public static final double kPreal = 1.0; - public static final double kDreal = 0.0; + public static final double kRealP = 1.0; + public static final double kRealD = 0.0; // MODE == SIM // Feedforward constants - public static final double kSsim = 0.0; - public static final double kVsim = 0.03; - public static final double kAsim = 0.0; + public static final double kSimS = 0.0; + public static final double kSimV = 0.03; + public static final double kSimA = 0.0; // Feedback (PID) constants - public static final double kPsim = 0.0; - public static final double kDsim = 0.0; + public static final double kSimP = 0.0; + public static final double kSimD = 0.0; + // Simulation plant constants + public static final double kSimGearing = 1.0; + public static final double kSimMomentOfInertiaKgMetersSq = + 0.5 * Pounds.of(1.5).in(Kilograms) * Math.pow(Inches.of(4.0).in(Meters), 2.0); } /************************************************************************* */ @@ -398,14 +435,14 @@ public static final class AutoConstants { // PathPlanner Config constants public static final RobotConfig kPathPlannerConfig = new RobotConfig( - RobotConstants.kRobotMass.in(Kilograms), - RobotConstants.kRobotMOI, + RobotConstants.kMass.in(Kilograms), + RobotConstants.kMomentOfInertiaKgMetersSq, new ModuleConfig( DrivebaseConstants.kWheelRadiusMeters, - DrivebaseConstants.kMaxLinearSpeed, - RobotConstants.kWheelCOF, + DrivebaseConstants.kMaxLinearSpeedMetersPerSec, + RobotConstants.kWheelCoefficientOfFriction, DCMotor.getKrakenX60Foc(1).withReduction(SwerveConstants.kDriveGearRatio), - DrivebaseConstants.kSlipCurrent, + DrivebaseConstants.kSlipCurrentAmps, 1), Drive.getModuleTranslations()); @@ -459,22 +496,22 @@ public static class VisionConstants { public static final double kThetaStdDevCoefficient = 0.01; // Basic filtering thresholds - public static final double maxAmbiguity = 0.3; - public static final double maxZError = 0.75; + public static final double kMaxAmbiguity = 0.3; + public static final double kMaxZErrorMeters = 0.75; // Standard deviation baselines, for 1 meter distance and 1 tag // (Adjusted automatically based on distance and # of tags) - public static final double linearStdDevBaseline = 0.02; // Meters - public static final double angularStdDevBaseline = 0.06; // Radians + public static final double kLinearStdDevBaseline = 0.02; // Meters + public static final double kAngularStdDevBaseline = 0.06; // Radians // Multipliers to apply for MegaTag 2 observations - public static final double linearStdDevMegatag2Factor = 0.5; // More stable than full 3D solve - public static final double angularStdDevMegatag2Factor = + public static final double kLinearStdDevMegatag2Factor = 0.5; // More stable than full 3D solve + public static final double kAngularStdDevMegatag2Factor = Double.POSITIVE_INFINITY; // No rotation data available } /************************************************************************* */ - /** Vision Camera Posses ************************************************* */ + /** Vision Camera Poses ************************************************** */ public static final class Cameras { public record CameraConfig( String name, @@ -527,12 +564,12 @@ public record CameraConfig( } /************************************************************************* */ - /** Deploy Directoy Location Constants *********************************** */ + /** Deploy Directory Location Constants ********************************** */ public static final class DeployConstants { - public static final String apriltagDir = "apriltags"; - public static final String choreoDir = "choreo"; - public static final String pathplannerDir = "pathplanner"; - public static final String yagslDir = "swerve"; + public static final String kAprilTagDir = "apriltags"; + public static final String kChoreoDir = "choreo"; + public static final String kPathPlannerDir = "pathplanner"; + public static final String kYagslDir = "swerve"; } /***************************************************************************/ @@ -551,7 +588,7 @@ public static RobotType getRobot() { /** Get the current robot mode */ public static Mode getMode() { return switch (robotType) { - case DEVBOT, COMPBOT -> RobotBase.isReal() ? Mode.REAL : Mode.REPLAY; + case DEVBOT1, DEVBOT2, COMPBOT -> RobotBase.isReal() ? Mode.REAL : Mode.REPLAY; case SIMBOT -> Mode.SIM; }; } @@ -562,6 +599,11 @@ public static boolean isPureSim() { return getMode() == Mode.SIM && !isReplay; } + /** Return whether live tuning/debug logging is enabled. */ + public static boolean isTuningMode() { + return kTuningMode; + } + /** Get the current swerve drive type */ public static SwerveType getSwerveType() { return swerveType; diff --git a/src/main/java/frc/robot/FieldConstants.java b/src/main/java/frc/robot/FieldConstants.java index 34c356df..85283c38 100644 --- a/src/main/java/frc/robot/FieldConstants.java +++ b/src/main/java/frc/robot/FieldConstants.java @@ -24,7 +24,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import edu.wpi.first.apriltag.AprilTagFieldLayout; -import edu.wpi.first.apriltag.AprilTagFields; import edu.wpi.first.wpilibj.Filesystem; import java.io.IOException; import java.nio.file.Path; @@ -50,13 +49,12 @@ public class FieldConstants { public static final int aprilTagCount = fieldType.getLayout().getTags().size(); public static final AprilTagLayoutType defaultAprilTagType = fieldType; - public static final AprilTagFieldLayout aprilTagLayout = - AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField); + public static final AprilTagFieldLayout aprilTagLayout = fieldType.getLayout(); @Getter public enum AprilTagLayoutType { REBUILT_WELDED("2026-rebuilt-welded"), - REBUILT_ANDYMARK("2026-rebuilt-welded"), + REBUILT_ANDYMARK("2026-rebuilt-andymark"), REEFSCAPE_WELDED("2025-reefscape-welded"), REEFSCAPE_ANDYMARK("2025-reefscape-andymark"), CRESCENDO("2024-crescendo"), diff --git a/src/main/java/frc/robot/FieldState.java b/src/main/java/frc/robot/FieldState.java index 0a3f40e6..54322c6c 100644 --- a/src/main/java/frc/robot/FieldState.java +++ b/src/main/java/frc/robot/FieldState.java @@ -63,14 +63,9 @@ public static boolean isHubActive() { return false; } - // Read in the current match time & get alliance - // TODO: Since the ``DriverStation.getMatchTime()`` returns INT:: - // Return the approximate match time. The FMS does not send an - // official match time to the robots, but does send an approximate - // match time. The value will count down the time remaining in the - // current period (auto or teleop). Warning: This is not an official - // time (so it cannot be used to dispute ref calls or guarantee that - // a function will trigger before the match ends). + // Read the approximate match time and alliance. The FMS does not send an official match time to + // robots; DriverStation.getMatchTime() is approximate and should not be used to dispute ref + // calls or guarantee that a function will trigger before the match ends. double timeRemaining = DriverStation.getMatchTime(); Alliance alliance = DriverStation.getAlliance().orElse(Alliance.Blue); diff --git a/src/main/java/frc/robot/README b/src/main/java/frc/robot/README index 0d5dc599..2140849f 100644 --- a/src/main/java/frc/robot/README +++ b/src/main/java/frc/robot/README @@ -60,7 +60,8 @@ commands/ of teams. generated/ - This directory holds the ``TunerConstants.java`` file produced by Phoenix + This directory holds the generated CTRE tuner constants selected by + ``TunerFactory.java``. These files are produced by Phoenix Tuner X's swerve generator (you must generate this file for your specific drivetrain -- the one included in the Az-RBSI is an example only). It must be modified as described in the AdvantageKit documentation (removal of diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java index 0dbae5e0..1f5f73cc 100644 --- a/src/main/java/frc/robot/Robot.java +++ b/src/main/java/frc/robot/Robot.java @@ -34,9 +34,6 @@ import org.littletonrobotics.junction.wpilog.WPILOGReader; import org.littletonrobotics.junction.wpilog.WPILOGWriter; import org.littletonrobotics.urcl.URCL; -import org.photonvision.PhotonCamera; -import org.photonvision.simulation.PhotonCameraSim; -import org.photonvision.simulation.VisionSystemSim; /** * The VM is configured to automatically run this class, and to call the functions corresponding to @@ -45,12 +42,12 @@ * project. */ public class Robot extends LoggedRobot { - private Command m_autoCommandPathPlanner; + private static final int TIMING_LOG_PERIOD_LOOPS = 5; + + private Command m_autonomousCommand; private RobotContainer m_robotContainer; private Timer m_disabledTimer; - - // Define simulation fields here - private VisionSystemSim visionSim; + private int timingLogLoops = 0; /** * This function is run when the robot is first started up and should be used for any @@ -59,7 +56,7 @@ public class Robot extends LoggedRobot { public Robot() { // Record metadata Logger.recordMetadata("Robot", Constants.getRobot().toString()); - Logger.recordMetadata("TuningMode", Boolean.toString(Constants.tuningMode)); + Logger.recordMetadata("TuningMode", Boolean.toString(Constants.isTuningMode())); Logger.recordMetadata("RuntimeType", getRuntimeType().toString()); Logger.recordMetadata("ProjectName", BuildConstants.MAVEN_NAME); Logger.recordMetadata("BuildDate", BuildConstants.BUILD_DATE); @@ -99,7 +96,7 @@ public Robot() { // Initialize URCL Logger.registerURCL(URCL.startExternal()); StatusLogger.disableAutoLogging(); // Disable REVLib's built-in logging - LoggedPowerDistribution.getInstance(PowerConstants.kPDMCANid, PowerConstants.kPDMType); + LoggedPowerDistribution.getInstance(PowerConstants.kPdmCanId, PowerConstants.kPdmType); // Start AdvantageKit logger Logger.start(); @@ -147,9 +144,12 @@ public void robotPeriodic() { } final long t4 = System.nanoTime(); - Logger.recordOutput("LogPeriodic/CodeLoop/RobotPeriodicMS", (t4 - t0) / 1e6); - Logger.recordOutput("LogPeriodic/CodeLoop/VirtualMS", (t2 - t1) / 1e6); - Logger.recordOutput("LogPeriodic/CodeLoop/SchedulerMS", (t3 - t2) / 1e6); + if (++timingLogLoops >= TIMING_LOG_PERIOD_LOOPS) { + timingLogLoops = 0; + Logger.recordOutput("LogPeriodic/CodeLoop/RobotPeriodicMS", (t4 - t0) / 1e6); + Logger.recordOutput("LogPeriodic/CodeLoop/VirtualMS", (t2 - t1) / 1e6); + Logger.recordOutput("LogPeriodic/CodeLoop/SchedulerMS", (t3 - t2) / 1e6); + } } /** This function is called once when the robot is disabled. */ @@ -166,7 +166,7 @@ public void disabledInit() { @Override public void disabledPeriodic() { // After WHEEL_LOCK_TIME has elapsed, release the drive brakes - if (m_disabledTimer.hasElapsed(Constants.DrivebaseConstants.kWheelLockTime)) { + if (m_disabledTimer.hasElapsed(Constants.DrivebaseConstants.kWheelLockTimeSecs)) { m_robotContainer.getDrivebase().setMotorBrake(false); m_disabledTimer.stop(); } @@ -182,26 +182,20 @@ public void autonomousInit() { m_robotContainer.getDrivebase().resetHeadingController(); m_robotContainer.getVision().resetPoseGate(TimeUtil.now()); - // TODO: Make sure Gyro inits here with whatever is in the path planning thingie - switch (Constants.getAutoType()) { - case MANUAL: - CommandScheduler.getInstance().schedule(m_robotContainer.getManualAuto()); - break; - - case PATHPLANNER: - m_autoCommandPathPlanner = m_robotContainer.getAutonomousCommandPathPlanner(); - // schedule the autonomous command - if (m_autoCommandPathPlanner != null) { - CommandScheduler.getInstance().schedule(m_autoCommandPathPlanner); - } - break; - - case CHOREO: - m_robotContainer.getAutonomousCommandChoreo(); - break; - default: - throw new RuntimeException( - "Incorrect AUTO type selected in Constants: " + Constants.getAutoType()); + // Do not zero the gyro here. PathPlanner and Choreo reset through Drive.resetPose(...), which + // aligns the pose estimator to the selected auto's start while preserving the gyro reference. + m_autonomousCommand = + switch (Constants.getAutoType()) { + case MANUAL -> m_robotContainer.getManualAuto(); + case PATHPLANNER -> m_robotContainer.getAutonomousCommandPathPlanner(); + case CHOREO -> m_robotContainer.getAutonomousCommandChoreo(); + default -> + throw new RuntimeException( + "Incorrect AUTO type selected in Constants: " + Constants.getAutoType()); + }; + + if (m_autonomousCommand != null) { + CommandScheduler.getInstance().schedule(m_autonomousCommand); } } @@ -216,10 +210,9 @@ public void teleopInit() { // teleop starts running. If you want the autonomous to // continue until interrupted by another command, remove // this line or comment it out. - if (m_autoCommandPathPlanner != null) { - m_autoCommandPathPlanner.cancel(); - } else { - CommandScheduler.getInstance().cancelAll(); + if (m_autonomousCommand != null) { + m_autonomousCommand.cancel(); + m_autonomousCommand = null; } m_robotContainer.getDrivebase().setMotorBrake(true); m_robotContainer.getDrivebase().resetHeadingController(); @@ -272,42 +265,4 @@ public void testInit() { /** This function is called periodically during test mode. */ @Override public void testPeriodic() {} - - /** This function is called once when the robot is first started up. */ - @Override - public void simulationInit() { - // ---------------- SIM-ONLY: vision simulation world + camera sims ---------------- - // 1) Create the vision simulation world - visionSim = new VisionSystemSim("CameraSweepWorld"); - - // 2) Add AprilTags (field layout) - visionSim.addAprilTags(FieldConstants.aprilTagLayout); - - // 3) Build PhotonCameraSim objects from Constants camera configs - final Constants.Cameras.CameraConfig[] camConfigs = Constants.Cameras.ALL; - - PhotonCameraSim[] simCams = new PhotonCameraSim[camConfigs.length]; - - for (int i = 0; i < camConfigs.length; i++) { - final var cfg = camConfigs[i]; - - // Name must match the VisionIOPhotonVisionSim name - PhotonCamera photonCam = new PhotonCamera(cfg.name()); - - // 2026 API: wrap camera with sim properties from Constants - PhotonCameraSim camSim = new PhotonCameraSim(photonCam, cfg.simProps()); - - // Register camera with the sim using the robot-to-camera transform - visionSim.addCamera(camSim, cfg.robotToCamera()); - - simCams[i] = camSim; - } - } - - /** This function is called periodically whilst in simulation. */ - @Override - public void simulationPeriodic() { - // Update sim each sim tick - visionSim.update(m_robotContainer.getDrivebase().getPose()); - } } diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index dfd7d65e..e1228a4c 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -19,7 +19,6 @@ package frc.robot; -import choreo.auto.AutoChooser; import choreo.auto.AutoFactory; import choreo.auto.AutoRoutine; import choreo.auto.AutoTrajectory; @@ -27,17 +26,14 @@ import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Transform2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.Filesystem; import edu.wpi.first.wpilibj.GenericHID; import edu.wpi.first.wpilibj.XboxController; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.button.CommandJoystick; import edu.wpi.first.wpilibj2.command.button.CommandXboxController; -import edu.wpi.first.wpilibj2.command.button.RobotModeTriggers; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.Constants.CANBuses; import frc.robot.Constants.Cameras; @@ -46,6 +42,7 @@ import frc.robot.commands.AutopilotCommands; import frc.robot.commands.DriveCommands; import frc.robot.subsystems.accelerometer.Accelerometer; +import frc.robot.subsystems.accelerometer.RioAccelIO; import frc.robot.subsystems.drive.Drive; import frc.robot.subsystems.drive.DriveOdometry; import frc.robot.subsystems.drive.SwerveConstants; @@ -53,6 +50,7 @@ import frc.robot.subsystems.flywheel_example.FlywheelIO; import frc.robot.subsystems.flywheel_example.FlywheelIOSim; import frc.robot.subsystems.imu.Imu; +import frc.robot.subsystems.imu.ImuIO; import frc.robot.subsystems.imu.ImuIOSim; import frc.robot.subsystems.vision.CameraSweepEvaluator; import frc.robot.subsystems.vision.Vision; @@ -62,7 +60,6 @@ import frc.robot.subsystems.vision.VisionIOPhotonVisionSim; import frc.robot.util.Alert; import frc.robot.util.Alert.AlertType; -import frc.robot.util.GetJoystickValue; import frc.robot.util.LoggedTunableNumber; import frc.robot.util.OverrideSwitches; import frc.robot.util.RBSICANBusRegistry; @@ -121,11 +118,12 @@ public class RobotContainer { // AutoChoosers for both supported path planning types private final LoggedDashboardChooser autoChooserPathPlanner; + private final LoggedDashboardChooser autoChooserChoreo; + private final AutoFactory autoFactoryChoreo; + private final LoggedDashboardChooser driveStyle = new LoggedDashboardChooser<>("Drive Style"); - private final AutoChooser autoChooserChoreo; - private final AutoFactory autoFactoryChoreo; // Input estimated battery capacity (if full, use printed value) private final LoggedTunableNumber batteryCapacity = new LoggedTunableNumber("Battery Amp-Hours", 18.0); @@ -176,24 +174,23 @@ public RobotContainer() { m_flywheel = new Flywheel(new FlywheelIOSim()); m_accel = new Accelerometer(m_imu); - // CameraSweepEvaluator (sim-only analysis) - VisionSystemSim visionSim = new VisionSystemSim("CameraSweepWorld"); - visionSim.addAprilTags(FieldConstants.aprilTagLayout); - var cams = Cameras.ALL; - PhotonCameraSim[] simCams = new PhotonCameraSim[cams.length]; - for (int i = 0; i < cams.length; i++) { - var cfg = cams[i]; - PhotonCamera photonCam = new PhotonCamera(cfg.name()); - PhotonCameraSim camSim = new PhotonCameraSim(photonCam, cfg.simProps()); - visionSim.addCamera(camSim, cfg.robotToCamera()); - simCams[i] = camSim; - } - - // Create the sweep evaluator (expects two cameras; adapt if you add more later) - if (simCams.length >= 2) { + // CameraSweepEvaluator uses isolated sweep-only Photon cameras to avoid colliding with + // robot vision camera names in NetworkTables. + if (Constants.getVisionType() == frc.robot.util.RBSIEnum.VisionType.PHOTON + && Cameras.ALL.length >= 2) { + VisionSystemSim visionSim = new VisionSystemSim("CameraSweepWorld"); + visionSim.addAprilTags(FieldConstants.aprilTagLayout); + PhotonCameraSim[] simCams = new PhotonCameraSim[Cameras.ALL.length]; + for (int i = 0; i < Cameras.ALL.length; i++) { + var cfg = Cameras.ALL[i]; + PhotonCamera photonCam = new PhotonCamera("Sweep_" + cfg.name()); + PhotonCameraSim camSim = new PhotonCameraSim(photonCam, cfg.simProps()); + visionSim.addCamera(camSim, cfg.robotToCamera()); + simCams[i] = camSim; + } sweep = new CameraSweepEvaluator(visionSim, simCams[0], simCams[1]); } else { - sweep = null; // or throw if you require exactly 2 cameras + sweep = null; } break; @@ -201,7 +198,7 @@ public RobotContainer() { default: // Replayed robot, disable IO implementations RBSICANBusRegistry.initSim(CANBuses.RIO, CANBuses.DRIVE); - m_imu = new Imu(new ImuIOSim() {}); + m_imu = new Imu(new ImuIO() {}); m_drivebase = new Drive(m_imu); m_driveOdometry = new DriveOdometry(m_drivebase, m_imu, m_drivebase.getModules()); m_vision = @@ -209,7 +206,7 @@ public RobotContainer() { m_drivebase, m_drivebase::addVisionMeasurement, buildVisionIOsReplay(m_drivebase)); m_flywheel = new Flywheel(new FlywheelIO() {}); - m_accel = new Accelerometer(m_imu); + m_accel = new Accelerometer(m_imu, RioAccelIO.noop()); sweep = null; break; } @@ -222,6 +219,9 @@ public RobotContainer() { // include ``m_drivebase``, as that is automatically monitored. m_power = new RBSIPowerMonitor(batteryCapacity, m_flywheel); + // Define PathPlanner named commands before any autos or paths are created. + defineAutoCommands(); + // Set up the SmartDashboard Auto Chooser based on auto type switch (Constants.getAutoType()) { case MANUAL: @@ -245,14 +245,15 @@ public RobotContainer() { autoFactoryChoreo = new AutoFactory( m_drivebase::getPose, // A function that returns the current robot pose - m_drivebase::resetOdometry, // A function that resets the current robot pose to the + m_drivebase::resetPose, // A function that resets the current robot pose to the // provided Pose2d m_drivebase::followTrajectory, // The drive subsystem trajectory follower true, // If alliance flipping should be enabled m_drivebase // The drive subsystem ); - autoChooserChoreo = new AutoChooser(); - autoChooserChoreo.addRoutine("twoPieceAuto", this::twoPieceAuto); + autoChooserChoreo = new LoggedDashboardChooser<>("Choreo Auto Choices"); + autoChooserChoreo.addDefaultOption("Nothing", Commands.none()); + autoChooserChoreo.addOption("twoPieceAuto", twoPieceAuto().cmd()); // Set the others to null autoChooserPathPlanner = null; break; @@ -263,12 +264,16 @@ public RobotContainer() { "Incorrect AUTO type selected in Constants: " + Constants.getAutoType()); } - // Get drive style from the Dashboard Chooser - driveStyle.addDefaultOption("TANK", DriveStyle.TANK); - driveStyle.addOption("GAMER", DriveStyle.GAMER); + // Get drive style from the Dashboard Chooser. The constant controls the boot default, and the + // dashboard chooser lets teams swap stick layouts between drivers without recompiling. + driveStyle.addDefaultOption( + OperatorConstants.kDriveStyle.name(), OperatorConstants.kDriveStyle); + for (DriveStyle style : DriveStyle.values()) { + if (style != OperatorConstants.kDriveStyle) { + driveStyle.addOption(style.name(), style); + } + } - // Define Auto commands - defineAutoCommands(); // Define SysIs Routines definesysIdRoutines(); // Configure the button and trigger bindings @@ -289,60 +294,30 @@ private void defineAutoCommands() { */ private void configureBindings() { - // Send the proper joystick input based on driver preference -- Set this in `Constants.java` - GetJoystickValue driveStickY; - GetJoystickValue driveStickX; - GetJoystickValue turnStickX; - // OPTIONAL: Use the DashboardChooser rather than the Constants file for Drive Style - // switch (driveStyle.get()) { - switch (OperatorConstants.kDriveStyle) { - case GAMER: - driveStickY = driverController::getRightY; - driveStickX = driverController::getRightX; - turnStickX = driverController::getLeftX; - break; - default: // Includes case TANK - driveStickY = driverController::getLeftY; - driveStickX = driverController::getLeftX; - turnStickX = driverController::getRightX; - } - // SET STANDARD DRIVING AS DEFAULT COMMAND FOR THE DRIVEBASE m_drivebase.setDefaultCommand( DriveCommands.fieldRelativeDrive( - m_drivebase, - () -> -driveStickY.value(), - () -> -driveStickX.value(), - () -> -turnStickX.value())); + m_drivebase, () -> -getDriveStickY(), () -> -getDriveStickX(), () -> -getTurnStickX())); // ** Example Commands -- Remap, remove, or change as desired ** // Press B button while driving --> ROBOT-CENTRIC driverController .b() - .onTrue( - Commands.runOnce( - () -> - DriveCommands.robotRelativeDrive( - m_drivebase, - () -> -driveStickY.value(), - () -> -driveStickX.value(), - () -> turnStickX.value()), - m_drivebase)); + .whileTrue( + DriveCommands.robotRelativeDrive( + m_drivebase, + () -> -getDriveStickY(), + () -> -getDriveStickX(), + () -> -getTurnStickX())); // Press A button -> BRAKE - driverController - .a() - .whileTrue(Commands.runOnce(() -> m_drivebase.setMotorBrake(true), m_drivebase)); + driverController.a().onTrue(DriveCommands.setBrakeMode(m_drivebase, true)); // Press X button --> Stop with wheels in X-Lock position - driverController.x().onTrue(Commands.runOnce(m_drivebase::stopWithX, m_drivebase)); + driverController.x().whileTrue(DriveCommands.stopWithX(m_drivebase)); // Press Y button --> Manually Re-Zero the Gyro - driverController - .y() - .onTrue( - Commands.runOnce(m_drivebase::zeroHeadingForAlliance, m_drivebase) - .ignoringDisable(true)); + driverController.y().onTrue(DriveCommands.zeroHeadingForAlliance(m_drivebase)); // Press RIGHT BUMPER --> Run the example flywheel driverController @@ -353,18 +328,21 @@ private void configureBindings() { m_flywheel::stop, m_flywheel)); - // Press LEFT BUMPER --> Drive to a pose 10 feet closer to the BLUE ALLIANCE wall + // Press LEFT BUMPER --> Drive to a demo pose offset defined in OperatorConstants driverController .leftBumper() .whileTrue( Commands.defer( () -> { - // New pose 2 feet closer to BLUE ALLIANCE wall + // Demo target relative to the current pose. Pose2d pose = m_drivebase .getPose() .transformBy( - new Transform2d(Units.feetToMeters(-10.0), 0.0, Rotation2d.kZero)); + new Transform2d( + OperatorConstants.kAutopilotDemoXOffsetMeters, + 0.0, + Rotation2d.kZero)); // Alternatively, you could define a pose in a separate module and call it here. // @@ -380,14 +358,23 @@ private void configureBindings() { driverController .povLeft() .whileTrue( - Commands.startEnd( - () -> { - m_drivebase.runVelocity( - new ChassisSpeeds(Units.inchesToMeters(0.), Units.inchesToMeters(11.0), 0.)); - }, - // Stop when command ended - m_drivebase::stop, - m_drivebase)); + DriveCommands.robotRelativeNudge( + m_drivebase, 0.0, OperatorConstants.kRobotRelativeNudgeSpeedMetersPerSec, 0.0)); + driverController + .povRight() + .whileTrue( + DriveCommands.robotRelativeNudge( + m_drivebase, 0.0, -OperatorConstants.kRobotRelativeNudgeSpeedMetersPerSec, 0.0)); + driverController + .povUp() + .whileTrue( + DriveCommands.robotRelativeNudge( + m_drivebase, OperatorConstants.kRobotRelativeNudgeSpeedMetersPerSec, 0.0, 0.0)); + driverController + .povDown() + .whileTrue( + DriveCommands.robotRelativeNudge( + m_drivebase, -OperatorConstants.kRobotRelativeNudgeSpeedMetersPerSec, 0.0, 0.0)); if (Constants.getMode() == Mode.SIM) { // IN SIMULATION ONLY: @@ -406,7 +393,7 @@ private void configureBindings() { .resolve("camera_sweep.csv") .toString()); } catch (Exception e) { - e.printStackTrace(); + DriverStation.reportError("Camera sweep failed", e.getStackTrace()); } })); } @@ -443,12 +430,8 @@ public Command getAutonomousCommandPathPlanner() { * * @return the command to run in autonomous */ - public void getAutonomousCommandChoreo() { - // Put the auto chooser on the dashboard - SmartDashboard.putData(autoChooserChoreo); - - // Schedule the selected auto during the autonomous period - RobotModeTriggers.autonomous().whileTrue(autoChooserChoreo.selectedCommandScheduler()); + public Command getAutonomousCommandChoreo() { + return autoChooserChoreo.get(); } /** Updates the alerts. */ @@ -504,17 +487,29 @@ private void definesysIdRoutines() { // Example Flywheel SysId Characterization autoChooserPathPlanner.addOption( - "Flywheel SysId (Quasistatic Forward)", - m_flywheel.sysIdQuasistatic(SysIdRoutine.Direction.kForward)); + "Flywheel SysId Voltage (Quasistatic Forward)", + m_flywheel.sysIdVoltageQuasistatic(SysIdRoutine.Direction.kForward)); + autoChooserPathPlanner.addOption( + "Flywheel SysId Voltage (Quasistatic Reverse)", + m_flywheel.sysIdVoltageQuasistatic(SysIdRoutine.Direction.kReverse)); + autoChooserPathPlanner.addOption( + "Flywheel SysId Voltage (Dynamic Forward)", + m_flywheel.sysIdVoltageDynamic(SysIdRoutine.Direction.kForward)); + autoChooserPathPlanner.addOption( + "Flywheel SysId Voltage (Dynamic Reverse)", + m_flywheel.sysIdVoltageDynamic(SysIdRoutine.Direction.kReverse)); autoChooserPathPlanner.addOption( - "Flywheel SysId (Quasistatic Reverse)", - m_flywheel.sysIdQuasistatic(SysIdRoutine.Direction.kReverse)); + "Flywheel SysId Duty Cycle (Quasistatic Forward)", + m_flywheel.sysIdDutyCycleQuasistatic(SysIdRoutine.Direction.kForward)); autoChooserPathPlanner.addOption( - "Flywheel SysId (Dynamic Forward)", - m_flywheel.sysIdDynamic(SysIdRoutine.Direction.kForward)); + "Flywheel SysId Duty Cycle (Quasistatic Reverse)", + m_flywheel.sysIdDutyCycleQuasistatic(SysIdRoutine.Direction.kReverse)); autoChooserPathPlanner.addOption( - "Flywheel SysId (Dynamic Reverse)", - m_flywheel.sysIdDynamic(SysIdRoutine.Direction.kReverse)); + "Flywheel SysId Duty Cycle (Dynamic Forward)", + m_flywheel.sysIdDutyCycleDynamic(SysIdRoutine.Direction.kForward)); + autoChooserPathPlanner.addOption( + "Flywheel SysId Duty Cycle (Dynamic Reverse)", + m_flywheel.sysIdDutyCycleDynamic(SysIdRoutine.Direction.kReverse)); } } @@ -538,13 +533,23 @@ private VisionIO[] buildVisionIOsReal(Drive drive) { // Vision Factories (SIM) private VisionIO[] buildVisionIOsSim(Drive drive) { - var cams = Constants.Cameras.ALL; - VisionIO[] ios = new VisionIO[cams.length]; - for (int i = 0; i < cams.length; i++) { - var cfg = cams[i]; - ios[i] = new VisionIOPhotonVisionSim(cfg.name(), cfg.robotToCamera(), drive::getPose); - } - return ios; + return switch (Constants.getVisionType()) { + case PHOTON -> + Arrays.stream(Constants.Cameras.ALL) + .map( + c -> + (VisionIO) + new VisionIOPhotonVisionSim( + c.name(), c.robotToCamera(), c.simProps(), drive::getPose)) + .toArray(VisionIO[]::new); + + case LIMELIGHT -> + Arrays.stream(Constants.Cameras.ALL) + .map(c -> (VisionIO) new VisionIOLimelight(c.name(), drive::getHeading)) + .toArray(VisionIO[]::new); + + case NONE -> new VisionIO[] {}; + }; } // Vision Factories (REPLAY) @@ -594,4 +599,30 @@ private AutoRoutine twoPieceAuto() { return routine; } + + private DriveStyle getSelectedDriveStyle() { + DriveStyle selected = driveStyle.get(); + return selected != null ? selected : OperatorConstants.kDriveStyle; + } + + private double getDriveStickY() { + return switch (getSelectedDriveStyle()) { + case GAMER -> driverController.getRightY(); + case TANK -> driverController.getLeftY(); + }; + } + + private double getDriveStickX() { + return switch (getSelectedDriveStyle()) { + case GAMER -> driverController.getRightX(); + case TANK -> driverController.getLeftX(); + }; + } + + private double getTurnStickX() { + return switch (getSelectedDriveStyle()) { + case GAMER -> driverController.getLeftX(); + case TANK -> driverController.getRightX(); + }; + } } diff --git a/src/main/java/frc/robot/commands/AutopilotCommands.java b/src/main/java/frc/robot/commands/AutopilotCommands.java index 80901f83..3b800eba 100644 --- a/src/main/java/frc/robot/commands/AutopilotCommands.java +++ b/src/main/java/frc/robot/commands/AutopilotCommands.java @@ -17,7 +17,7 @@ package frc.robot.commands; -import static edu.wpi.first.units.Units.*; +import static edu.wpi.first.units.Units.RadiansPerSecond; import com.therekrab.autopilot.APTarget; import com.therekrab.autopilot.Autopilot.APResult; @@ -31,7 +31,8 @@ import frc.robot.subsystems.drive.Drive; import org.littletonrobotics.junction.Logger; -public class AutopilotCommands { +public final class AutopilotCommands { + private AutopilotCommands() {} /** * AutoPilot Command Factory -- just reference pose @@ -179,6 +180,7 @@ private static Command autopilotToTarget(Drive drive, APTarget target) { () -> { ChassisSpeeds robotRelativeSpeeds = drive.getChassisSpeeds(); Pose2d pose = drive.getPose(); + boolean atTarget = AutoConstants.kAutopilot.atTarget(pose, target); Logger.recordOutput("Autopilot/CurrentPose", pose); Logger.recordOutput("Autopilot/FinalPose", target.getReference()); @@ -192,8 +194,7 @@ private static Command autopilotToTarget(Drive drive, APTarget target) { Logger.recordOutput("Autopilot/outputVx", output.vx()); Logger.recordOutput("Autopilot/outputVy", output.vy()); Logger.recordOutput("Autopilot/targetAngle", output.targetAngle()); - Logger.recordOutput( - "Autopilot/atTarget", AutoConstants.kAutopilot.atTarget(drive.getPose(), target)); + Logger.recordOutput("Autopilot/atTarget", atTarget); // Output is field relative ChassisSpeeds speeds = @@ -214,6 +215,10 @@ private static Command autopilotToTarget(Drive drive, APTarget target) { // Reset PID controller when command starts & ends; run until we're at target .beforeStarting(() -> drive.resetHeadingController()) .until(() -> AutoConstants.kAutopilot.atTarget(drive.getPose(), target)) - .finallyDo(() -> drive.resetHeadingController()); + .finallyDo( + () -> { + drive.stop(); + drive.resetHeadingController(); + }); } } diff --git a/src/main/java/frc/robot/commands/ChoreoAutoController.java b/src/main/java/frc/robot/commands/ChoreoAutoController.java index 483f9afb..6b41f69f 100644 --- a/src/main/java/frc/robot/commands/ChoreoAutoController.java +++ b/src/main/java/frc/robot/commands/ChoreoAutoController.java @@ -9,29 +9,45 @@ package frc.robot.commands; -import static frc.robot.Constants.AutoConstants.*; - import choreo.trajectory.SwerveSample; import edu.wpi.first.math.controller.PIDController; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.kinematics.ChassisSpeeds; +import frc.robot.Constants.AutoConstants; import frc.robot.subsystems.drive.Drive; import java.util.function.Consumer; -public class ChoreoAutoController implements Consumer { +public final class ChoreoAutoController implements Consumer { private final Drive drive; // drive subsystem private final PIDController xController = - new PIDController(kChoreoDrivePID.kP, kChoreoDrivePID.kI, kChoreoDrivePID.kD); + new PIDController( + AutoConstants.kChoreoDrivePID.kP, + AutoConstants.kChoreoDrivePID.kI, + AutoConstants.kChoreoDrivePID.kD); private final PIDController yController = - new PIDController(kChoreoDrivePID.kP, kChoreoDrivePID.kI, kChoreoDrivePID.kD); + new PIDController( + AutoConstants.kChoreoDrivePID.kP, + AutoConstants.kChoreoDrivePID.kI, + AutoConstants.kChoreoDrivePID.kD); private final PIDController headingController = - new PIDController(kChoreoSteerPID.kP, kChoreoSteerPID.kI, kChoreoSteerPID.kD); + new PIDController( + AutoConstants.kChoreoSteerPID.kP, + AutoConstants.kChoreoSteerPID.kI, + AutoConstants.kChoreoSteerPID.kD); public ChoreoAutoController(Drive drive) { this.drive = drive; headingController.enableContinuousInput(-Math.PI, Math.PI); } + public void reset() { + Pose2d pose = drive.getPose(); + xController.reset(); + yController.reset(); + headingController.reset(); + headingController.setSetpoint(pose.getRotation().getRadians()); + } + @Override public void accept(SwerveSample referenceState) { Pose2d pose = drive.getPose(); diff --git a/src/main/java/frc/robot/commands/DriveCommands.java b/src/main/java/frc/robot/commands/DriveCommands.java index 0a2a5e73..626c1932 100644 --- a/src/main/java/frc/robot/commands/DriveCommands.java +++ b/src/main/java/frc/robot/commands/DriveCommands.java @@ -11,9 +11,7 @@ import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.filter.SlewRateLimiter; -import edu.wpi.first.math.geometry.Pose2d; 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.kinematics.ChassisSpeeds; import edu.wpi.first.math.util.Units; @@ -22,24 +20,18 @@ import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; +import frc.robot.Constants.DrivebaseConstants; import frc.robot.Constants.OperatorConstants; import frc.robot.subsystems.drive.Drive; import frc.robot.subsystems.drive.SwerveConstants; import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.function.DoubleSupplier; import java.util.function.Supplier; -public class DriveCommands { - - // Needed for Characterization routines, not normal robot operations - // DO NOT ADJUST - private static final double FF_START_DELAY = 2.0; // Secs - private static final double FF_RAMP_RATE = 0.1; // Volts/Sec - private static final double WHEEL_RADIUS_MAX_VELOCITY = 0.25; // Rad/Sec - private static final double WHEEL_RADIUS_RAMP_RATE = 0.05; // Rad/Sec^2 +public final class DriveCommands { private DriveCommands() {} @@ -51,30 +43,30 @@ public static Command fieldRelativeDrive( DoubleSupplier xSupplier, DoubleSupplier ySupplier, DoubleSupplier omegaSupplier) { + SlewRateLimiter xLimiter = joystickLimiter(); + SlewRateLimiter yLimiter = joystickLimiter(); + SlewRateLimiter omegaLimiter = joystickLimiter(); + return Commands.run( - () -> { - // Get the Linear Velocity & Omega from inputs - Translation2d linearVelocity = - getLinearVelocity(xSupplier.getAsDouble(), ySupplier.getAsDouble()); - double omega = getOmega(omegaSupplier.getAsDouble()); - - // Convert to field relative speeds & send command - ChassisSpeeds speeds = - new ChassisSpeeds( - linearVelocity.getX() * drive.getMaxLinearSpeedMetersPerSec(), - linearVelocity.getY() * drive.getMaxLinearSpeedMetersPerSec(), - omega * drive.getMaxAngularSpeedRadPerSec()); - boolean isFlipped = - DriverStation.getAlliance().isPresent() - && DriverStation.getAlliance().get() == Alliance.Red; - drive.runVelocity( - ChassisSpeeds.fromFieldRelativeSpeeds( - speeds, - isFlipped - ? drive.getHeading().plus(new Rotation2d(Math.PI)) - : drive.getHeading())); - }, - drive); + () -> { + // Get the Linear Velocity & Omega from inputs + Translation2d linearVelocity = + getLinearVelocity( + xLimiter.calculate(xSupplier.getAsDouble()), + yLimiter.calculate(ySupplier.getAsDouble())); + double omega = getOmega(omegaLimiter.calculate(omegaSupplier.getAsDouble())); + + // Convert to field relative speeds & send command + ChassisSpeeds speeds = + new ChassisSpeeds( + linearVelocity.getX() * drive.getMaxLinearSpeedMetersPerSec(), + linearVelocity.getY() * drive.getMaxLinearSpeedMetersPerSec(), + omega * drive.getMaxAngularSpeedRadPerSec()); + drive.runVelocity( + ChassisSpeeds.fromFieldRelativeSpeeds(speeds, getAllianceAwareHeading(drive))); + }, + drive) + .beforeStarting(() -> resetLimiters(xLimiter, yLimiter, omegaLimiter)); } /** @@ -85,21 +77,28 @@ public static Command robotRelativeDrive( DoubleSupplier xSupplier, DoubleSupplier ySupplier, DoubleSupplier omegaSupplier) { + SlewRateLimiter xLimiter = joystickLimiter(); + SlewRateLimiter yLimiter = joystickLimiter(); + SlewRateLimiter omegaLimiter = joystickLimiter(); + return Commands.run( - () -> { - // Get the Linear Velocity & Omega from inputs - Translation2d linearVelocity = - getLinearVelocity(xSupplier.getAsDouble(), ySupplier.getAsDouble()); - double omega = getOmega(omegaSupplier.getAsDouble()); - - // Run with straight-up velocities w.r.t. the robot! - drive.runVelocity( - new ChassisSpeeds( - linearVelocity.getX() * drive.getMaxLinearSpeedMetersPerSec(), - linearVelocity.getY() * drive.getMaxLinearSpeedMetersPerSec(), - omega * drive.getMaxAngularSpeedRadPerSec())); - }, - drive); + () -> { + // Get the Linear Velocity & Omega from inputs + Translation2d linearVelocity = + getLinearVelocity( + xLimiter.calculate(xSupplier.getAsDouble()), + yLimiter.calculate(ySupplier.getAsDouble())); + double omega = getOmega(omegaLimiter.calculate(omegaSupplier.getAsDouble())); + + // Run with straight-up velocities w.r.t. the robot! + drive.runVelocity( + new ChassisSpeeds( + linearVelocity.getX() * drive.getMaxLinearSpeedMetersPerSec(), + linearVelocity.getY() * drive.getMaxLinearSpeedMetersPerSec(), + omega * drive.getMaxAngularSpeedRadPerSec())); + }, + drive) + .beforeStarting(() -> resetLimiters(xLimiter, yLimiter, omegaLimiter)); } /** @@ -112,13 +111,17 @@ public static Command fieldRelativeDriveAtAngle( DoubleSupplier xSupplier, DoubleSupplier ySupplier, Supplier rotationSupplier) { + SlewRateLimiter xLimiter = joystickLimiter(); + SlewRateLimiter yLimiter = joystickLimiter(); // Construct command return Commands.run( () -> { // Get linear velocity Translation2d linearVelocity = - getLinearVelocity(xSupplier.getAsDouble(), ySupplier.getAsDouble()); + getLinearVelocity( + xLimiter.calculate(xSupplier.getAsDouble()), + yLimiter.calculate(ySupplier.getAsDouble())); // Calculate angular speed double omega = @@ -133,52 +136,95 @@ public static Command fieldRelativeDriveAtAngle( linearVelocity.getX() * drive.getMaxLinearSpeedMetersPerSec(), linearVelocity.getY() * drive.getMaxLinearSpeedMetersPerSec(), omega); - boolean isFlipped = - DriverStation.getAlliance().isPresent() - && DriverStation.getAlliance().get() == Alliance.Red; drive.runVelocity( - ChassisSpeeds.fromFieldRelativeSpeeds( - speeds, - isFlipped - ? drive.getHeading().plus(new Rotation2d(Math.PI)) - : drive.getHeading())); + ChassisSpeeds.fromFieldRelativeSpeeds(speeds, getAllianceAwareHeading(drive))); }, drive) // Reset PID controller when command starts & ends; - .beforeStarting(() -> drive.resetHeadingController()) + .beforeStarting( + () -> { + resetLimiters(xLimiter, yLimiter); + drive.resetHeadingController(); + }) .finallyDo(() -> drive.resetHeadingController()); } + /** Holds the drive stopped while scheduled. */ + public static Command stop(Drive drive) { + return Commands.run(drive::stop, drive).finallyDo(drive::stop); + } + + /** Holds the modules in an X pattern while scheduled. */ + public static Command stopWithX(Drive drive) { + return Commands.run(drive::stopWithX, drive).finallyDo(drive::stop); + } + + /** Sets drive motor neutral mode. */ + public static Command setBrakeMode(Drive drive, boolean brake) { + return Commands.runOnce(() -> drive.setMotorBrake(brake), drive).ignoringDisable(true); + } + + /** Zeros field-relative heading using alliance-aware orientation. */ + public static Command zeroHeadingForAlliance(Drive drive) { + return Commands.runOnce(drive::zeroHeadingForAlliance, drive).ignoringDisable(true); + } + + /** Drives robot-relative at a fixed velocity while scheduled. */ + public static Command robotRelativeNudge( + Drive drive, double vxMetersPerSec, double vyMetersPerSec, double omegaRadPerSec) { + ChassisSpeeds speeds = new ChassisSpeeds(vxMetersPerSec, vyMetersPerSec, omegaRadPerSec); + return Commands.run(() -> drive.runVelocity(speeds), drive).finallyDo(drive::stop); + } + /** Utility functions needed by commands in this module ****************** */ /** * Compute the new linear velocity from inputs, including applying deadbands and squaring for - * smoothness. Also apply the linear velocity Slew Rate Limiter. + * smoothness. Slew limiting is applied by the command factory before calling this helper. */ - private static Translation2d getLinearVelocity(double x, double y) { + static Translation2d getLinearVelocity(double x, double y) { // Apply deadband - double linearMagnitude = MathUtil.applyDeadband(Math.hypot(x, y), OperatorConstants.kDeadband); - Rotation2d linearDirection = new Rotation2d(Math.atan2(y, x)); + double linearMagnitude = + MathUtil.applyDeadband(Math.hypot(x, y), OperatorConstants.kJoystickDeadband); + if (linearMagnitude <= 0.0) { + return Translation2d.kZero; + } // Square magnitude for more precise control // NOTE: The x & y values range from -1 to +1, so their squares are as well - linearMagnitude = linearMagnitude * linearMagnitude; + double scaledMagnitude = linearMagnitude * linearMagnitude; + double inputMagnitude = Math.hypot(x, y); + double scale = inputMagnitude > 1e-9 ? scaledMagnitude / inputMagnitude : 0.0; // Return new linear velocity - return new Pose2d(Translation2d.kZero, linearDirection) - .transformBy(new Transform2d(linearMagnitude, 0.0, Rotation2d.kZero)) - .getTranslation(); + return new Translation2d(x * scale, y * scale); } /** * Compute the new angular velocity from inputs, including applying deadbands and squaring for - * smoothness. Also apply the angular Slew Rate Limiter. + * smoothness. Slew limiting is applied by the command factory before calling this helper. */ - private static double getOmega(double omega) { - omega = MathUtil.applyDeadband(omega, OperatorConstants.kDeadband); + static double getOmega(double omega) { + omega = MathUtil.applyDeadband(omega, OperatorConstants.kJoystickDeadband); return Math.copySign(omega * omega, omega); } + private static SlewRateLimiter joystickLimiter() { + return new SlewRateLimiter(OperatorConstants.kJoystickSlewRateLimit); + } + + private static void resetLimiters(SlewRateLimiter... limiters) { + for (SlewRateLimiter limiter : limiters) { + limiter.reset(0.0); + } + } + + private static Rotation2d getAllianceAwareHeading(Drive drive) { + return DriverStation.getAlliance().orElse(Alliance.Blue) == Alliance.Red + ? drive.getHeading().plus(Rotation2d.k180deg) + : drive.getHeading(); + } + /***************************************************************************/ /** DRIVEBASE CHARACTERIZATION COMMANDS ********************************** */ /** @@ -187,131 +233,158 @@ private static double getOmega(double omega) { *

This command should only be used in voltage control mode. */ public static Command feedforwardCharacterization(Drive drive) { - List velocitySamples = new LinkedList<>(); - List voltageSamples = new LinkedList<>(); + List velocitySamples = new ArrayList<>(); + List voltageSamples = new ArrayList<>(); Timer timer = new Timer(); return Commands.sequence( - // Reset data - Commands.runOnce( - () -> { - velocitySamples.clear(); - voltageSamples.clear(); - }), - - // Allow modules to orient - Commands.run( - () -> { - drive.runCharacterization(0.0); - }, - drive) - .withTimeout(FF_START_DELAY), - - // Start timer - Commands.runOnce(timer::restart), - - // Accelerate and gather data - Commands.run( - () -> { - double voltage = timer.get() * FF_RAMP_RATE; - drive.runCharacterization(voltage); - velocitySamples.add(drive.getFFCharacterizationVelocity()); - voltageSamples.add(voltage); - }, - drive) - - // When cancelled, calculate and print results - .finallyDo( - () -> { - int n = velocitySamples.size(); - double sumX = 0.0; - double sumY = 0.0; - double sumXY = 0.0; - double sumX2 = 0.0; - for (int i = 0; i < n; i++) { - sumX += velocitySamples.get(i); - sumY += voltageSamples.get(i); - sumXY += velocitySamples.get(i) * voltageSamples.get(i); - sumX2 += velocitySamples.get(i) * velocitySamples.get(i); - } - double kS = (sumY * sumX2 - sumX * sumXY) / (n * sumX2 - sumX * sumX); - double kV = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); - - NumberFormat formatter = new DecimalFormat("#0.00000"); - System.out.println("********** Drive FF Characterization Results **********"); - System.out.println("\tkS: " + formatter.format(kS)); - System.out.println("\tkV: " + formatter.format(kV)); - })); - } - - /** Measures the robot's wheel radius by spinning in a circle. */ - public static Command wheelRadiusCharacterization(Drive drive) { - SlewRateLimiter limiter = new SlewRateLimiter(WHEEL_RADIUS_RAMP_RATE); - WheelRadiusCharacterizationState state = new WheelRadiusCharacterizationState(); - - return Commands.parallel( - // Drive control sequence - Commands.sequence( - // Reset acceleration limiter + // Reset data Commands.runOnce( () -> { - limiter.reset(0.0); + velocitySamples.clear(); + voltageSamples.clear(); }), - // Turn in place, accelerating up to full speed + // Allow modules to orient Commands.run( - () -> { - double speed = limiter.calculate(WHEEL_RADIUS_MAX_VELOCITY); - drive.runVelocity(new ChassisSpeeds(0.0, 0.0, speed)); - }, - drive)), - - // Measurement sequence - Commands.sequence( - // Wait for modules to fully orient before starting measurement - Commands.waitSeconds(1.0), + () -> { + drive.runCharacterization(0.0); + }, + drive) + .withTimeout(DrivebaseConstants.kFeedforwardCharacterizationStartDelaySecs), - // Record starting measurement - Commands.runOnce( - () -> { - state.positions = drive.getWheelRadiusCharacterizationPositions(); - state.lastAngle = drive.getHeading(); - state.gyroDelta = 0.0; - }), + // Start timer + Commands.runOnce(timer::restart), - // Update gyro delta + // Accelerate and gather data Commands.run( () -> { - var rotation = drive.getHeading(); - state.gyroDelta += Math.abs(rotation.minus(state.lastAngle).getRadians()); - state.lastAngle = rotation; - }) + double voltage = + timer.get() + * DrivebaseConstants.kFeedforwardCharacterizationRampRateVoltsPerSec; + drive.runCharacterization(voltage); + velocitySamples.add(drive.getFFCharacterizationVelocity()); + voltageSamples.add(voltage); + }, + drive) // When cancelled, calculate and print results .finallyDo( () -> { - double[] positions = drive.getWheelRadiusCharacterizationPositions(); - double wheelDelta = 0.0; - for (int i = 0; i < 4; i++) { - wheelDelta += Math.abs(positions[i] - state.positions[i]) / 4.0; + int n = velocitySamples.size(); + double sumX = 0.0; + double sumY = 0.0; + double sumXY = 0.0; + double sumX2 = 0.0; + for (int i = 0; i < n; i++) { + sumX += velocitySamples.get(i); + sumY += voltageSamples.get(i); + sumXY += velocitySamples.get(i) * voltageSamples.get(i); + sumX2 += velocitySamples.get(i) * velocitySamples.get(i); + } + double denominator = n * sumX2 - sumX * sumX; + if (n < 2 || Math.abs(denominator) < 1e-9) { + DriverStation.reportWarning( + "Drive FF characterization did not collect enough usable samples.", + false); + drive.stop(); + return; } - double wheelRadius = - (state.gyroDelta * SwerveConstants.kDriveBaseRadiusMeters) / wheelDelta; - - NumberFormat formatter = new DecimalFormat("#0.000"); - System.out.println( - "********** Wheel Radius Characterization Results **********"); - System.out.println( - "\tWheel Delta: " + formatter.format(wheelDelta) + " radians"); - System.out.println( - "\tGyro Delta: " + formatter.format(state.gyroDelta) + " radians"); - System.out.println( - "\tWheel Radius: " - + formatter.format(wheelRadius) - + " meters, " - + formatter.format(Units.metersToInches(wheelRadius)) - + " inches"); - }))); + + double kS = (sumY * sumX2 - sumX * sumXY) / denominator; + double kV = (n * sumXY - sumX * sumY) / denominator; + + NumberFormat formatter = new DecimalFormat("#0.00000"); + System.out.println("********** Drive FF Characterization Results **********"); + System.out.println("\tkS: " + formatter.format(kS)); + System.out.println("\tkV: " + formatter.format(kV)); + drive.stop(); + })) + .finallyDo(drive::stop); + } + + /** Measures the robot's wheel radius by spinning in a circle. */ + public static Command wheelRadiusCharacterization(Drive drive) { + SlewRateLimiter limiter = + new SlewRateLimiter(DrivebaseConstants.kWheelRadiusCharacterizationRampRateRadPerSecSq); + WheelRadiusCharacterizationState state = new WheelRadiusCharacterizationState(); + + return Commands.parallel( + // Drive control sequence + Commands.sequence( + // Reset acceleration limiter + Commands.runOnce( + () -> { + limiter.reset(0.0); + }), + + // Turn in place, accelerating up to full speed + Commands.run( + () -> { + double speed = + limiter.calculate( + DrivebaseConstants.kWheelRadiusCharacterizationMaxVelocityRadPerSec); + drive.runVelocity(new ChassisSpeeds(0.0, 0.0, speed)); + }, + drive)), + + // Measurement sequence + Commands.sequence( + // Wait for modules to fully orient before starting measurement + Commands.waitSeconds(DrivebaseConstants.kWheelRadiusCharacterizationStartDelaySecs), + + // Record starting measurement + Commands.runOnce( + () -> { + state.positions = drive.getWheelRadiusCharacterizationPositions(); + state.lastAngle = drive.getHeading(); + state.gyroDelta = 0.0; + }), + + // Update gyro delta + Commands.run( + () -> { + var rotation = drive.getHeading(); + state.gyroDelta += Math.abs(rotation.minus(state.lastAngle).getRadians()); + state.lastAngle = rotation; + }) + + // When cancelled, calculate and print results + .finallyDo( + () -> { + double[] positions = drive.getWheelRadiusCharacterizationPositions(); + double wheelDelta = 0.0; + for (int i = 0; i < 4; i++) { + wheelDelta += Math.abs(positions[i] - state.positions[i]) / 4.0; + } + if (wheelDelta <= 1e-9) { + DriverStation.reportWarning( + "Wheel radius characterization did not measure wheel movement.", + false); + drive.stop(); + return; + } + + double wheelRadius = + (state.gyroDelta * SwerveConstants.kDriveBaseRadiusMeters) + / wheelDelta; + + NumberFormat formatter = new DecimalFormat("#0.000"); + System.out.println( + "********** Wheel Radius Characterization Results **********"); + System.out.println( + "\tWheel Delta: " + formatter.format(wheelDelta) + " radians"); + System.out.println( + "\tGyro Delta: " + formatter.format(state.gyroDelta) + " radians"); + System.out.println( + "\tWheel Radius: " + + formatter.format(wheelRadius) + + " meters, " + + formatter.format(Units.metersToInches(wheelRadius)) + + " inches"); + drive.stop(); + }))) + .finallyDo(drive::stop); } private static class WheelRadiusCharacterizationState { diff --git a/src/main/java/frc/robot/generated/COMPBOTTunerConstants.java b/src/main/java/frc/robot/generated/COMPBOTTunerConstants.java new file mode 100644 index 00000000..91b48bfe --- /dev/null +++ b/src/main/java/frc/robot/generated/COMPBOTTunerConstants.java @@ -0,0 +1,315 @@ +package frc.robot.generated; + +import static edu.wpi.first.units.Units.*; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.*; +import com.ctre.phoenix6.hardware.*; +import com.ctre.phoenix6.signals.*; +import com.ctre.phoenix6.swerve.*; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.*; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.units.measure.*; + +// import frc.robot.subsystems.CommandSwerveDrivetrain; + +// Generated by the 2026 Tuner X Swerve Project Generator +// https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/tuner-swerve/index.html +public class COMPBOTTunerConstants { + // Both sets of gains need to be tuned to your individual robot. + + // The steer motor uses any SwerveModule.SteerRequestType control request with the + // output type specified by SwerveModuleConstants.SteerMotorClosedLoopOutput + private static final Slot0Configs steerGains = + new Slot0Configs() + .withKP(100) + .withKI(0) + .withKD(0.5) + .withKS(0.1) + .withKV(2.66) + .withKA(0) + .withStaticFeedforwardSign(StaticFeedforwardSignValue.UseClosedLoopSign); + // When using closed-loop control, the drive motor uses the control + // output type specified by SwerveModuleConstants.DriveMotorClosedLoopOutput + private static final Slot0Configs driveGains = + new Slot0Configs().withKP(0.1).withKI(0).withKD(0).withKS(0).withKV(0.124); + + // The closed-loop output type to use for the steer motors; + // This affects the PID/FF gains for the steer motors + private static final ClosedLoopOutputType kSteerClosedLoopOutput = ClosedLoopOutputType.Voltage; + // The closed-loop output type to use for the drive motors; + // This affects the PID/FF gains for the drive motors + private static final ClosedLoopOutputType kDriveClosedLoopOutput = ClosedLoopOutputType.Voltage; + + // The type of motor used for the drive motor + private static final DriveMotorArrangement kDriveMotorType = + DriveMotorArrangement.TalonFX_Integrated; + // The type of motor used for the drive motor + private static final SteerMotorArrangement kSteerMotorType = + SteerMotorArrangement.TalonFX_Integrated; + + // The remote sensor feedback type to use for the steer motors; + // When not Pro-licensed, Fused*/Sync* automatically fall back to Remote* + private static final SteerFeedbackType kSteerFeedbackType = SteerFeedbackType.FusedCANcoder; + + // The stator current at which the wheels start to slip; + // This needs to be tuned to your individual robot + private static final Current kSlipCurrent = Amps.of(120.0); + + // Initial configs for the drive and steer motors and the azimuth encoder; these cannot be null. + // Some configs will be overwritten; check the `with*InitialConfigs()` API documentation. + private static final TalonFXConfiguration driveInitialConfigs = new TalonFXConfiguration(); + private static final TalonFXConfiguration steerInitialConfigs = + new TalonFXConfiguration() + .withCurrentLimits( + new CurrentLimitsConfigs() + // Swerve azimuth does not require much torque output, so we can set a relatively + // low + // stator current limit to help avoid brownouts without impacting performance. + .withStatorCurrentLimit(Amps.of(60)) + .withStatorCurrentLimitEnable(true)); + private static final CANcoderConfiguration encoderInitialConfigs = new CANcoderConfiguration(); + // Configs for the Pigeon 2; leave this null to skip applying Pigeon 2 configs + private static final Pigeon2Configuration pigeonConfigs = null; + + // CAN bus that the devices are located on; + // All swerve devices must share the same CAN bus + public static final CANBus kCANBus = new CANBus("DriveTrain", "./logs/example.hoot"); + + // Theoretical free speed (m/s) at 12 V applied output; + // This needs to be tuned to your individual robot + public static final LinearVelocity kSpeedAt12Volts = MetersPerSecond.of(4.73); + + // Every 1 rotation of the azimuth results in kCoupleRatio drive motor turns; + // This may need to be tuned to your individual robot + private static final double kCoupleRatio = 3.5714285714285716; + + private static final double kDriveGearRatio = 6.746031746031747; + private static final double kSteerGearRatio = 21.428571428571427; + private static final Distance kWheelRadius = Inches.of(2); + + private static final boolean kInvertLeftSide = false; + private static final boolean kInvertRightSide = true; + + private static final int kPigeonId = 13; + + // These are only used for simulation + private static final MomentOfInertia kSteerInertia = KilogramSquareMeters.of(0.004); + private static final MomentOfInertia kDriveInertia = KilogramSquareMeters.of(0.025); + // Simulated voltage necessary to overcome friction + private static final Voltage kSteerFrictionVoltage = Volts.of(0.2); + private static final Voltage kDriveFrictionVoltage = Volts.of(0.2); + + public static final SwerveDrivetrainConstants DrivetrainConstants = + new SwerveDrivetrainConstants() + .withCANBusName(kCANBus.getName()) + .withPigeon2Id(kPigeonId) + .withPigeon2Configs(pigeonConfigs); + + private static final SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + ConstantCreator = + new SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration>() + .withDriveMotorGearRatio(kDriveGearRatio) + .withSteerMotorGearRatio(kSteerGearRatio) + .withCouplingGearRatio(kCoupleRatio) + .withWheelRadius(kWheelRadius) + .withSteerMotorGains(steerGains) + .withDriveMotorGains(driveGains) + .withSteerMotorClosedLoopOutput(kSteerClosedLoopOutput) + .withDriveMotorClosedLoopOutput(kDriveClosedLoopOutput) + .withSlipCurrent(kSlipCurrent) + .withSpeedAt12Volts(kSpeedAt12Volts) + .withDriveMotorType(kDriveMotorType) + .withSteerMotorType(kSteerMotorType) + .withFeedbackSource(kSteerFeedbackType) + .withDriveMotorInitialConfigs(driveInitialConfigs) + .withSteerMotorInitialConfigs(steerInitialConfigs) + .withEncoderInitialConfigs(encoderInitialConfigs) + .withSteerInertia(kSteerInertia) + .withDriveInertia(kDriveInertia) + .withSteerFrictionVoltage(kSteerFrictionVoltage) + .withDriveFrictionVoltage(kDriveFrictionVoltage); + + // Front Left + private static final int kFrontLeftDriveMotorId = 1; + private static final int kFrontLeftSteerMotorId = 2; + private static final int kFrontLeftEncoderId = 3; + private static final Angle kFrontLeftEncoderOffset = Rotations.of(0.0); + private static final boolean kFrontLeftSteerMotorInverted = true; + private static final boolean kFrontLeftEncoderInverted = false; + + private static final Distance kFrontLeftXPos = Inches.of(10.0); + private static final Distance kFrontLeftYPos = Inches.of(10.0); + + // Front Right + private static final int kFrontRightDriveMotorId = 4; + private static final int kFrontRightSteerMotorId = 5; + private static final int kFrontRightEncoderId = 6; + private static final Angle kFrontRightEncoderOffset = Rotations.of(0.0); + private static final boolean kFrontRightSteerMotorInverted = true; + private static final boolean kFrontRightEncoderInverted = false; + + private static final Distance kFrontRightXPos = Inches.of(10.0); + private static final Distance kFrontRightYPos = Inches.of(-10.0); + + // Back Left + private static final int kBackLeftDriveMotorId = 7; + private static final int kBackLeftSteerMotorId = 8; + private static final int kBackLeftEncoderId = 9; + private static final Angle kBackLeftEncoderOffset = Rotations.of(0.0); + private static final boolean kBackLeftSteerMotorInverted = true; + private static final boolean kBackLeftEncoderInverted = false; + + private static final Distance kBackLeftXPos = Inches.of(-10.0); + private static final Distance kBackLeftYPos = Inches.of(10.0); + + // Back Right + private static final int kBackRightDriveMotorId = 10; + private static final int kBackRightSteerMotorId = 11; + private static final int kBackRightEncoderId = 12; + private static final Angle kBackRightEncoderOffset = Rotations.of(0.0); + private static final boolean kBackRightSteerMotorInverted = true; + private static final boolean kBackRightEncoderInverted = false; + + private static final Distance kBackRightXPos = Inches.of(-10.0); + private static final Distance kBackRightYPos = Inches.of(-10.0); + + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontLeft = + ConstantCreator.createModuleConstants( + kFrontLeftSteerMotorId, + kFrontLeftDriveMotorId, + kFrontLeftEncoderId, + kFrontLeftEncoderOffset, + kFrontLeftXPos, + kFrontLeftYPos, + kInvertLeftSide, + kFrontLeftSteerMotorInverted, + kFrontLeftEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontRight = + ConstantCreator.createModuleConstants( + kFrontRightSteerMotorId, + kFrontRightDriveMotorId, + kFrontRightEncoderId, + kFrontRightEncoderOffset, + kFrontRightXPos, + kFrontRightYPos, + kInvertRightSide, + kFrontRightSteerMotorInverted, + kFrontRightEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackLeft = + ConstantCreator.createModuleConstants( + kBackLeftSteerMotorId, + kBackLeftDriveMotorId, + kBackLeftEncoderId, + kBackLeftEncoderOffset, + kBackLeftXPos, + kBackLeftYPos, + kInvertLeftSide, + kBackLeftSteerMotorInverted, + kBackLeftEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackRight = + ConstantCreator.createModuleConstants( + kBackRightSteerMotorId, + kBackRightDriveMotorId, + kBackRightEncoderId, + kBackRightEncoderOffset, + kBackRightXPos, + kBackRightYPos, + kInvertRightSide, + kBackRightSteerMotorInverted, + kBackRightEncoderInverted); + + // /** + // * Creates a CommandSwerveDrivetrain instance. This should only be called once in your robot + // * program,. + // */ + // public static CommandSwerveDrivetrain createDrivetrain() { + // return new CommandSwerveDrivetrain( + // DrivetrainConstants, FrontLeft, FrontRight, BackLeft, BackRight); + // } + + /** Swerve Drive class utilizing CTR Electronics' Phoenix 6 API with the selected device types. */ + public static class TunerSwerveDrivetrain extends SwerveDrivetrain { + /** + * Constructs a CTRE SwerveDrivetrain using the specified constants. + * + *

This constructs the underlying hardware devices, so users should not construct the devices + * themselves. If they need the devices, they can access them through getters in the classes. + * + * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + * @param modules Constants for each specific module + */ + public TunerSwerveDrivetrain( + SwerveDrivetrainConstants drivetrainConstants, SwerveModuleConstants... modules) { + super(TalonFX::new, TalonFX::new, CANcoder::new, drivetrainConstants, modules); + } + + /** + * Constructs a CTRE SwerveDrivetrain using the specified constants. + * + *

This constructs the underlying hardware devices, so users should not construct the devices + * themselves. If they need the devices, they can access them through getters in the classes. + * + * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + * @param odometryUpdateFrequency The frequency to run the odometry loop. If unspecified or set + * to 0 Hz, this is 250 Hz on CAN FD, and 100 Hz on CAN 2.0. + * @param modules Constants for each specific module + */ + public TunerSwerveDrivetrain( + SwerveDrivetrainConstants drivetrainConstants, + double odometryUpdateFrequency, + SwerveModuleConstants... modules) { + super( + TalonFX::new, + TalonFX::new, + CANcoder::new, + drivetrainConstants, + odometryUpdateFrequency, + modules); + } + + /** + * Constructs a CTRE SwerveDrivetrain using the specified constants. + * + *

This constructs the underlying hardware devices, so users should not construct the devices + * themselves. If they need the devices, they can access them through getters in the classes. + * + * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + * @param odometryUpdateFrequency The frequency to run the odometry loop. If unspecified or set + * to 0 Hz, this is 250 Hz on CAN FD, and 100 Hz on CAN 2.0. + * @param odometryStandardDeviation The standard deviation for odometry calculation in the form + * [x, y, theta]áµ€, with units in meters and radians + * @param visionStandardDeviation The standard deviation for vision calculation in the form [x, + * y, theta]áµ€, with units in meters and radians + * @param modules Constants for each specific module + */ + public TunerSwerveDrivetrain( + SwerveDrivetrainConstants drivetrainConstants, + double odometryUpdateFrequency, + Matrix odometryStandardDeviation, + Matrix visionStandardDeviation, + SwerveModuleConstants... modules) { + super( + TalonFX::new, + TalonFX::new, + CANcoder::new, + drivetrainConstants, + odometryUpdateFrequency, + odometryStandardDeviation, + visionStandardDeviation, + modules); + } + } +} diff --git a/src/main/java/frc/robot/generated/COMPBOTTunerView.java b/src/main/java/frc/robot/generated/COMPBOTTunerView.java new file mode 100644 index 00000000..480ab82a --- /dev/null +++ b/src/main/java/frc/robot/generated/COMPBOTTunerView.java @@ -0,0 +1,43 @@ +package frc.robot.generated; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveDrivetrainConstants; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; + +public final class COMPBOTTunerView implements TunerView { + @Override + public CANBus canBus() { + return COMPBOTTunerConstants.kCANBus; + } + + @Override + public SwerveDrivetrainConstants drivetrain() { + return COMPBOTTunerConstants.DrivetrainConstants; + } + + @Override + public SwerveModuleConstants + frontLeft() { + return COMPBOTTunerConstants.FrontLeft; + } + + @Override + public SwerveModuleConstants + frontRight() { + return COMPBOTTunerConstants.FrontRight; + } + + @Override + public SwerveModuleConstants + backLeft() { + return COMPBOTTunerConstants.BackLeft; + } + + @Override + public SwerveModuleConstants + backRight() { + return COMPBOTTunerConstants.BackRight; + } +} diff --git a/src/main/java/frc/robot/generated/TunerConstants.java b/src/main/java/frc/robot/generated/DEVBOT1TunerConstants.java old mode 100644 new mode 100755 similarity index 93% rename from src/main/java/frc/robot/generated/TunerConstants.java rename to src/main/java/frc/robot/generated/DEVBOT1TunerConstants.java index 3e23fec1..0942cc39 --- a/src/main/java/frc/robot/generated/TunerConstants.java +++ b/src/main/java/frc/robot/generated/DEVBOT1TunerConstants.java @@ -15,9 +15,9 @@ // import frc.robot.subsystems.CommandSwerveDrivetrain; -// Generated by the 2026 Tuner X Swerve Project Generator +// Generated by the Tuner X Swerve Project Generator // https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/tuner-swerve/index.html -public class TunerConstants { +public class DEVBOT1TunerConstants { // Both sets of gains need to be tuned to your individual robot. // The steer motor uses any SwerveModule.SteerRequestType control request with the @@ -51,7 +51,7 @@ public class TunerConstants { SteerMotorArrangement.TalonFX_Integrated; // The remote sensor feedback type to use for the steer motors; - // When not Pro-licensed, Fused*/Sync* automatically fall back to Remote* + // When not Pro-licensed, FusedCANcoder/SyncCANcoder automatically fall back to RemoteCANcoder private static final SteerFeedbackType kSteerFeedbackType = SteerFeedbackType.FusedCANcoder; // The stator current at which the wheels start to slip; @@ -80,15 +80,15 @@ public class TunerConstants { // Theoretical free speed (m/s) at 12 V applied output; // This needs to be tuned to your individual robot - public static final LinearVelocity kSpeedAt12Volts = MetersPerSecond.of(5.21); + public static final LinearVelocity kSpeedAt12Volts = MetersPerSecond.of(4.73); // Every 1 rotation of the azimuth results in kCoupleRatio drive motor turns; // This may need to be tuned to your individual robot private static final double kCoupleRatio = 3.5714285714285716; - private static final double kDriveGearRatio = 6.122448979591837; + private static final double kDriveGearRatio = 6.746031746031747; private static final double kSteerGearRatio = 21.428571428571427; - private static final Distance kWheelRadius = Inches.of(2.0); + private static final Distance kWheelRadius = Inches.of(2); private static final boolean kInvertLeftSide = false; private static final boolean kInvertRightSide = true; @@ -138,45 +138,45 @@ public class TunerConstants { private static final int kFrontLeftDriveMotorId = 1; private static final int kFrontLeftSteerMotorId = 2; private static final int kFrontLeftEncoderId = 3; - private static final Angle kFrontLeftEncoderOffset = Rotations.of(0.143310546875); + private static final Angle kFrontLeftEncoderOffset = Rotations.of(0.0); private static final boolean kFrontLeftSteerMotorInverted = true; private static final boolean kFrontLeftEncoderInverted = false; - private static final Distance kFrontLeftXPos = Inches.of(11.375); - private static final Distance kFrontLeftYPos = Inches.of(11.375); + private static final Distance kFrontLeftXPos = Inches.of(10.0); + private static final Distance kFrontLeftYPos = Inches.of(10.0); // Front Right private static final int kFrontRightDriveMotorId = 4; private static final int kFrontRightSteerMotorId = 5; private static final int kFrontRightEncoderId = 6; - private static final Angle kFrontRightEncoderOffset = Rotations.of(-0.328125); + private static final Angle kFrontRightEncoderOffset = Rotations.of(0.0); private static final boolean kFrontRightSteerMotorInverted = true; private static final boolean kFrontRightEncoderInverted = false; - private static final Distance kFrontRightXPos = Inches.of(11.375); - private static final Distance kFrontRightYPos = Inches.of(-11.375); + private static final Distance kFrontRightXPos = Inches.of(10.0); + private static final Distance kFrontRightYPos = Inches.of(-10.0); // Back Left private static final int kBackLeftDriveMotorId = 7; private static final int kBackLeftSteerMotorId = 8; private static final int kBackLeftEncoderId = 9; - private static final Angle kBackLeftEncoderOffset = Rotations.of(0.135498046875); + private static final Angle kBackLeftEncoderOffset = Rotations.of(0.0); private static final boolean kBackLeftSteerMotorInverted = true; private static final boolean kBackLeftEncoderInverted = false; - private static final Distance kBackLeftXPos = Inches.of(-11.375); - private static final Distance kBackLeftYPos = Inches.of(11.375); + private static final Distance kBackLeftXPos = Inches.of(-10.0); + private static final Distance kBackLeftYPos = Inches.of(10.0); // Back Right private static final int kBackRightDriveMotorId = 10; private static final int kBackRightSteerMotorId = 11; private static final int kBackRightEncoderId = 12; - private static final Angle kBackRightEncoderOffset = Rotations.of(0.478515625); + private static final Angle kBackRightEncoderOffset = Rotations.of(0.0); private static final boolean kBackRightSteerMotorInverted = true; private static final boolean kBackRightEncoderInverted = false; - private static final Distance kBackRightXPos = Inches.of(-11.375); - private static final Distance kBackRightYPos = Inches.of(-11.375); + private static final Distance kBackRightXPos = Inches.of(-10.0); + private static final Distance kBackRightYPos = Inches.of(-10.0); public static final SwerveModuleConstants< TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> diff --git a/src/main/java/frc/robot/generated/DEVBOT1TunerView.java b/src/main/java/frc/robot/generated/DEVBOT1TunerView.java new file mode 100644 index 00000000..46d19bca --- /dev/null +++ b/src/main/java/frc/robot/generated/DEVBOT1TunerView.java @@ -0,0 +1,43 @@ +package frc.robot.generated; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveDrivetrainConstants; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; + +public final class DEVBOT1TunerView implements TunerView { + @Override + public CANBus canBus() { + return DEVBOT1TunerConstants.kCANBus; + } + + @Override + public SwerveDrivetrainConstants drivetrain() { + return DEVBOT1TunerConstants.DrivetrainConstants; + } + + @Override + public SwerveModuleConstants + frontLeft() { + return DEVBOT1TunerConstants.FrontLeft; + } + + @Override + public SwerveModuleConstants + frontRight() { + return DEVBOT1TunerConstants.FrontRight; + } + + @Override + public SwerveModuleConstants + backLeft() { + return DEVBOT1TunerConstants.BackLeft; + } + + @Override + public SwerveModuleConstants + backRight() { + return DEVBOT1TunerConstants.BackRight; + } +} diff --git a/src/main/java/frc/robot/generated/DEVBOT2TunerConstants.java b/src/main/java/frc/robot/generated/DEVBOT2TunerConstants.java new file mode 100644 index 00000000..31b86369 --- /dev/null +++ b/src/main/java/frc/robot/generated/DEVBOT2TunerConstants.java @@ -0,0 +1,322 @@ +package frc.robot.generated; + +import static edu.wpi.first.units.Units.*; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.*; +import com.ctre.phoenix6.signals.*; +import com.ctre.phoenix6.swerve.*; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.*; +import edu.wpi.first.units.measure.*; + +// import frc.robot.subsystems.CommandSwerveDrivetrain; + +// Generated by the 2026 Tuner X Swerve Project Generator +// https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/tuner-swerve/index.html +public class DEVBOT2TunerConstants { + // Both sets of gains need to be tuned to your individual robot. + + // The steer motor uses any SwerveModule.SteerRequestType control request with the + // output type specified by SwerveModuleConstants.SteerMotorClosedLoopOutput + private static final Slot0Configs steerGains = + new Slot0Configs() + .withKP(100) + .withKI(0) + .withKD(0.5) + .withKS(0.1) + .withKV(2.66) + .withKA(0) + .withStaticFeedforwardSign(StaticFeedforwardSignValue.UseClosedLoopSign); + // When using closed-loop control, the drive motor uses the control + // output type specified by SwerveModuleConstants.DriveMotorClosedLoopOutput + private static final Slot0Configs driveGains = + new Slot0Configs().withKP(0.1).withKI(0).withKD(0).withKS(0).withKV(0.124); + + // The closed-loop output type to use for the steer motors; + // This affects the PID/FF gains for the steer motors + private static final ClosedLoopOutputType kSteerClosedLoopOutput = ClosedLoopOutputType.Voltage; + // The closed-loop output type to use for the drive motors; + // This affects the PID/FF gains for the drive motors + private static final ClosedLoopOutputType kDriveClosedLoopOutput = ClosedLoopOutputType.Voltage; + + // The type of motor used for the drive motor + private static final DriveMotorArrangement kDriveMotorType = + DriveMotorArrangement.TalonFX_Integrated; + // The type of motor used for the drive motor + private static final SteerMotorArrangement kSteerMotorType = + SteerMotorArrangement.TalonFX_Integrated; + + // The remote sensor feedback type to use for the steer motors; + // When not Pro-licensed, Fused*/Sync* automatically fall back to Remote* + private static final SteerFeedbackType kSteerFeedbackType = SteerFeedbackType.FusedCANcoder; + + // The stator current at which the wheels start to slip; + // This needs to be tuned to your individual robot + private static final Current kSlipCurrent = Amps.of(120.0); + + // Initial configs for the drive and steer motors and the azimuth encoder; these cannot be null. + // Some configs will be overwritten; check the `with*InitialConfigs()` API documentation. + private static final TalonFXConfiguration driveInitialConfigs = new TalonFXConfiguration(); + private static final TalonFXConfiguration steerInitialConfigs = + new TalonFXConfiguration() + .withCurrentLimits( + new CurrentLimitsConfigs() + // Swerve azimuth does not require much torque output, so we can set a relatively + // low + // stator current limit to help avoid brownouts without impacting performance. + .withStatorCurrentLimit(Amps.of(60)) + .withStatorCurrentLimitEnable(true)); + private static final CANcoderConfiguration encoderInitialConfigs = new CANcoderConfiguration(); + // Configs for the Pigeon 2; leave this null to skip applying Pigeon 2 configs + private static final Pigeon2Configuration pigeonConfigs = null; + + // CAN bus that the devices are located on; + // All swerve devices must share the same CAN bus + public static final CANBus kCANBus = new CANBus("DriveTrain", "./logs/example.hoot"); + + // Theoretical free speed (m/s) at 12 V applied output; + // This needs to be tuned to your individual robot + public static final LinearVelocity kSpeedAt12Volts = MetersPerSecond.of(4.73); + + // Every 1 rotation of the azimuth results in kCoupleRatio drive motor turns; + // This may need to be tuned to your individual robot + private static final double kCoupleRatio = 3.5714285714285716; + + private static final double kDriveGearRatio = 6.746031746031747; + private static final double kSteerGearRatio = 21.428571428571427; + private static final Distance kWheelRadius = Inches.of(2); + + private static final boolean kInvertLeftSide = false; + private static final boolean kInvertRightSide = true; + + private static final int kPigeonId = 13; + + // These are only used for simulation + private static final MomentOfInertia kSteerInertia = KilogramSquareMeters.of(0.004); + private static final MomentOfInertia kDriveInertia = KilogramSquareMeters.of(0.025); + // Simulated voltage necessary to overcome friction + private static final Voltage kSteerFrictionVoltage = Volts.of(0.2); + private static final Voltage kDriveFrictionVoltage = Volts.of(0.2); + + public static final SwerveDrivetrainConstants DrivetrainConstants = + new SwerveDrivetrainConstants() + .withCANBusName(kCANBus.getName()) + .withPigeon2Id(kPigeonId) + .withPigeon2Configs(pigeonConfigs); + + private static final SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + ConstantCreator = + new SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration>() + .withDriveMotorGearRatio(kDriveGearRatio) + .withSteerMotorGearRatio(kSteerGearRatio) + .withCouplingGearRatio(kCoupleRatio) + .withWheelRadius(kWheelRadius) + .withSteerMotorGains(steerGains) + .withDriveMotorGains(driveGains) + .withSteerMotorClosedLoopOutput(kSteerClosedLoopOutput) + .withDriveMotorClosedLoopOutput(kDriveClosedLoopOutput) + .withSlipCurrent(kSlipCurrent) + .withSpeedAt12Volts(kSpeedAt12Volts) + .withDriveMotorType(kDriveMotorType) + .withSteerMotorType(kSteerMotorType) + .withFeedbackSource(kSteerFeedbackType) + .withDriveMotorInitialConfigs(driveInitialConfigs) + .withSteerMotorInitialConfigs(steerInitialConfigs) + .withEncoderInitialConfigs(encoderInitialConfigs) + .withSteerInertia(kSteerInertia) + .withDriveInertia(kDriveInertia) + .withSteerFrictionVoltage(kSteerFrictionVoltage) + .withDriveFrictionVoltage(kDriveFrictionVoltage); + + // Front Left + private static final int kFrontLeftDriveMotorId = 1; + private static final int kFrontLeftSteerMotorId = 2; + private static final int kFrontLeftEncoderId = 3; + private static final Angle kFrontLeftEncoderOffset = Rotations.of(0.0); + private static final boolean kFrontLeftSteerMotorInverted = true; + private static final boolean kFrontLeftEncoderInverted = false; + + private static final Distance kFrontLeftXPos = Inches.of(10.0); + private static final Distance kFrontLeftYPos = Inches.of(10.0); + + // Front Right + private static final int kFrontRightDriveMotorId = 4; + private static final int kFrontRightSteerMotorId = 5; + private static final int kFrontRightEncoderId = 6; + private static final Angle kFrontRightEncoderOffset = Rotations.of(0.0); + private static final boolean kFrontRightSteerMotorInverted = true; + private static final boolean kFrontRightEncoderInverted = false; + + private static final Distance kFrontRightXPos = Inches.of(10.0); + private static final Distance kFrontRightYPos = Inches.of(-10.0); + + // Back Left + private static final int kBackLeftDriveMotorId = 7; + private static final int kBackLeftSteerMotorId = 8; + private static final int kBackLeftEncoderId = 9; + private static final Angle kBackLeftEncoderOffset = Rotations.of(0.0); + private static final boolean kBackLeftSteerMotorInverted = true; + private static final boolean kBackLeftEncoderInverted = false; + + private static final Distance kBackLeftXPos = Inches.of(-10.0); + private static final Distance kBackLeftYPos = Inches.of(10.0); + + // Back Right + private static final int kBackRightDriveMotorId = 10; + private static final int kBackRightSteerMotorId = 11; + private static final int kBackRightEncoderId = 12; + private static final Angle kBackRightEncoderOffset = Rotations.of(0.0); + private static final boolean kBackRightSteerMotorInverted = true; + private static final boolean kBackRightEncoderInverted = false; + + private static final Distance kBackRightXPos = Inches.of(-10.0); + private static final Distance kBackRightYPos = Inches.of(-10.0); + + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontLeft = + ConstantCreator.createModuleConstants( + kFrontLeftSteerMotorId, + kFrontLeftDriveMotorId, + kFrontLeftEncoderId, + kFrontLeftEncoderOffset, + kFrontLeftXPos, + kFrontLeftYPos, + kInvertLeftSide, + kFrontLeftSteerMotorInverted, + kFrontLeftEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontRight = + ConstantCreator.createModuleConstants( + kFrontRightSteerMotorId, + kFrontRightDriveMotorId, + kFrontRightEncoderId, + kFrontRightEncoderOffset, + kFrontRightXPos, + kFrontRightYPos, + kInvertRightSide, + kFrontRightSteerMotorInverted, + kFrontRightEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackLeft = + ConstantCreator.createModuleConstants( + kBackLeftSteerMotorId, + kBackLeftDriveMotorId, + kBackLeftEncoderId, + kBackLeftEncoderOffset, + kBackLeftXPos, + kBackLeftYPos, + kInvertLeftSide, + kBackLeftSteerMotorInverted, + kBackLeftEncoderInverted); + public static final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackRight = + ConstantCreator.createModuleConstants( + kBackRightSteerMotorId, + kBackRightDriveMotorId, + kBackRightEncoderId, + kBackRightEncoderOffset, + kBackRightXPos, + kBackRightYPos, + kInvertRightSide, + kBackRightSteerMotorInverted, + kBackRightEncoderInverted); + + // /** + // * Creates a CommandSwerveDrivetrain instance. + // * This should only be called once in your robot program,. + // */ + // public static CommandSwerveDrivetrain createDrivetrain() { + // return new CommandSwerveDrivetrain( + // DrivetrainConstants, FrontLeft, FrontRight, BackLeft, BackRight + // ); + // } + + // /** + // * Swerve Drive class utilizing CTR Electronics' Phoenix 6 API with the selected device types. + // */ + // public static class TunerSwerveDrivetrain extends SwerveDrivetrain + // { + // /** + // * Constructs a CTRE SwerveDrivetrain using the specified constants. + // *

+ // * This constructs the underlying hardware devices, so users should not construct + // * the devices themselves. If they need the devices, they can access them through + // * getters in the classes. + // * + // * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + // * @param modules Constants for each specific module + // */ + // public TunerSwerveDrivetrain( + // SwerveDrivetrainConstants drivetrainConstants, + // SwerveModuleConstants... modules + // ) { + // super( + // TalonFX::new, TalonFX::new, CANcoder::new, + // drivetrainConstants, modules + // ); + // } + + // /** + // * Constructs a CTRE SwerveDrivetrain using the specified constants. + // *

+ // * This constructs the underlying hardware devices, so users should not construct + // * the devices themselves. If they need the devices, they can access them through + // * getters in the classes. + // * + // * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + // * @param odometryUpdateFrequency The frequency to run the odometry loop. If + // * unspecified or set to 0 Hz, this is 250 Hz on + // * CAN FD, and 100 Hz on CAN 2.0. + // * @param modules Constants for each specific module + // */ + // public TunerSwerveDrivetrain( + // SwerveDrivetrainConstants drivetrainConstants, + // double odometryUpdateFrequency, + // SwerveModuleConstants... modules + // ) { + // super( + // TalonFX::new, TalonFX::new, CANcoder::new, + // drivetrainConstants, odometryUpdateFrequency, modules + // ); + // } + + // /** + // * Constructs a CTRE SwerveDrivetrain using the specified constants. + // *

+ // * This constructs the underlying hardware devices, so users should not construct + // * the devices themselves. If they need the devices, they can access them through + // * getters in the classes. + // * + // * @param drivetrainConstants Drivetrain-wide constants for the swerve drive + // * @param odometryUpdateFrequency The frequency to run the odometry loop. If + // * unspecified or set to 0 Hz, this is 250 Hz on + // * CAN FD, and 100 Hz on CAN 2.0. + // * @param odometryStandardDeviation The standard deviation for odometry calculation + // * in the form [x, y, theta]T, with units in meters + // * and radians + // * @param visionStandardDeviation The standard deviation for vision calculation + // * in the form [x, y, theta]T, with units in meters + // * and radians + // * @param modules Constants for each specific module + // */ + // public TunerSwerveDrivetrain( + // SwerveDrivetrainConstants drivetrainConstants, + // double odometryUpdateFrequency, + // Matrix odometryStandardDeviation, + // Matrix visionStandardDeviation, + // SwerveModuleConstants... modules + // ) { + // super( + // TalonFX::new, TalonFX::new, CANcoder::new, + // drivetrainConstants, odometryUpdateFrequency, + // odometryStandardDeviation, visionStandardDeviation, modules + // ); + // } + // } +} diff --git a/src/main/java/frc/robot/generated/DEVBOT2TunerView.java b/src/main/java/frc/robot/generated/DEVBOT2TunerView.java new file mode 100644 index 00000000..18a905b5 --- /dev/null +++ b/src/main/java/frc/robot/generated/DEVBOT2TunerView.java @@ -0,0 +1,43 @@ +package frc.robot.generated; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveDrivetrainConstants; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; + +public final class DEVBOT2TunerView implements TunerView { + @Override + public CANBus canBus() { + return DEVBOT2TunerConstants.kCANBus; + } + + @Override + public SwerveDrivetrainConstants drivetrain() { + return DEVBOT2TunerConstants.DrivetrainConstants; + } + + @Override + public SwerveModuleConstants + frontLeft() { + return DEVBOT2TunerConstants.FrontLeft; + } + + @Override + public SwerveModuleConstants + frontRight() { + return DEVBOT2TunerConstants.FrontRight; + } + + @Override + public SwerveModuleConstants + backLeft() { + return DEVBOT2TunerConstants.BackLeft; + } + + @Override + public SwerveModuleConstants + backRight() { + return DEVBOT2TunerConstants.BackRight; + } +} diff --git a/src/main/java/frc/robot/generated/README b/src/main/java/frc/robot/generated/README index 420d3fe4..bdaf5e8c 100644 --- a/src/main/java/frc/robot/generated/README +++ b/src/main/java/frc/robot/generated/README @@ -1,2 +1,69 @@ -This directory, and in particular the TunerConstants.java file, are the -Phoenix6 equivalent of the ``src/main/deploy/swerve`` directory for YAGSL. +Phoenix 6 generated swerve constants for RBSI +================================================ + +This package is the Phoenix 6 equivalent of the YAGSL `src/main/deploy/swerve` +configuration directory. + +RBSI keeps one generated TunerConstants file per robot: + +- `COMPBOTTunerConstants.java` +- `DEVBOT1TunerConstants.java` +- `DEVBOT2TunerConstants.java` + +The files are intentionally named per robot. Phoenix Tuner X normally generates +a file named `TunerConstants.java`; do not leave that generic name in this +package unless you also update the RBSI view classes. + +How to copy a new Phoenix Tuner X file into RBSI +------------------------------------------------ + +1. Run the Phoenix Tuner X Swerve Project Generator for the physical robot. +2. On the final page, choose the option that generates only `TunerConstants`. +3. Copy the generated file into this directory: + `src/main/java/frc/robot/generated/` +4. Rename it for the selected RBSI robot: + - `COMPBOTTunerConstants.java` for `Constants.RobotType.COMPBOT` + - `DEVBOT1TunerConstants.java` for `Constants.RobotType.DEVBOT1` + - `DEVBOT2TunerConstants.java` for `Constants.RobotType.DEVBOT2` +5. Edit the Java class declaration inside the copied file so it matches the new + filename. For example: + `public class COMPBOTTunerConstants` +6. Comment out the generated import for `CommandSwerveDrivetrain`. RBSI does + not use CTRE's generated command drivetrain class. +7. Comment out the generated `createDrivetrain()` helper. RBSI constructs and + owns its drive subsystem through `frc.robot.subsystems.drive.Drive`. +8. Leave `DrivetrainConstants`, `FrontLeft`, `FrontRight`, `BackLeft`, and + `BackRight` public. The RBSI view classes expose those constants to the rest + of the robot code. + +How the View classes work +------------------------- + +RBSI code should not import `COMPBOTTunerConstants`, `DEVBOT1TunerConstants`, or +`DEVBOT2TunerConstants` directly. + +Instead: + +- `TunerView` defines the small interface RBSI needs from any generated file. +- `COMPBOTTunerView`, `DEVBOT1TunerView`, and `DEVBOT2TunerView` adapt each + per-robot generated file to that interface. +- `TunerFactory` checks `Constants.getRobot()` and returns the correct view. +- Drive code uses `TunerFactory.INSTANCE` so module IO, odometry, and + `SwerveConstants` all read the selected robot's generated constants. + +When adding another robot type, add a matching `NewRobotTunerConstants.java`, a +`NewRobotTunerView.java`, and a `TunerFactory` switch case. + +Generic template values +----------------------- + +The template files in this repository are not calibrated for a real robot. They +are intentionally generic: + +- encoder offsets are all `Rotations.of(0.0)`, +- module locations are `+/-10.0` inches from the robot center, +- coupling, drive gear, steer gear, and wheel radius values match across the + three template files. + +After generating real constants, those template values should be replaced by +the measured values from Phoenix Tuner X. diff --git a/src/main/java/frc/robot/generated/TunerFactory.java b/src/main/java/frc/robot/generated/TunerFactory.java new file mode 100644 index 00000000..0432f49e --- /dev/null +++ b/src/main/java/frc/robot/generated/TunerFactory.java @@ -0,0 +1,21 @@ +package frc.robot.generated; + +import frc.robot.Constants; +import org.littletonrobotics.junction.Logger; + +public final class TunerFactory { + public static final TunerView INSTANCE = create(); + + private static TunerView create() { + // Debugging + Logger.recordOutput("Drive/EncoderOffsets/RobotType", Constants.getRobot()); + + // Return the proper view into TunerConstants + return switch (Constants.getRobot()) { + case DEVBOT2 -> new DEVBOT2TunerView(); + case DEVBOT1 -> new DEVBOT1TunerView(); + case COMPBOT -> new COMPBOTTunerView(); + default -> new COMPBOTTunerView(); + }; + } +} diff --git a/src/main/java/frc/robot/generated/TunerView.java b/src/main/java/frc/robot/generated/TunerView.java new file mode 100644 index 00000000..8069ddcf --- /dev/null +++ b/src/main/java/frc/robot/generated/TunerView.java @@ -0,0 +1,25 @@ +package frc.robot.generated; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveDrivetrainConstants; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; + +public interface TunerView { + CANBus canBus(); + + SwerveDrivetrainConstants drivetrain(); + + SwerveModuleConstants + frontLeft(); + + SwerveModuleConstants + frontRight(); + + SwerveModuleConstants + backLeft(); + + SwerveModuleConstants + backRight(); +} diff --git a/src/main/java/frc/robot/subsystems/accelerometer/Accelerometer.java b/src/main/java/frc/robot/subsystems/accelerometer/Accelerometer.java index e6ebe08d..5b7b4e25 100644 --- a/src/main/java/frc/robot/subsystems/accelerometer/Accelerometer.java +++ b/src/main/java/frc/robot/subsystems/accelerometer/Accelerometer.java @@ -16,6 +16,7 @@ import edu.wpi.first.math.geometry.Translation3d; import frc.robot.Constants; import frc.robot.Constants.RobotConstants; +import frc.robot.Constants.SensorConstants; import frc.robot.subsystems.imu.Imu; import frc.robot.util.TimeUtil; import frc.robot.util.VirtualSubsystem; @@ -37,7 +38,7 @@ public class Accelerometer extends VirtualSubsystem { // Variables needed during the periodic private Translation3d rawRio, rioAcc, rioJerk, imuAcc, imuJerk; - private Translation3d prevRioAcc = Translation3d.kZero; + private Translation3d prevRioAcc = null; // Log decimation private int loopCount = 0; @@ -48,8 +49,12 @@ public class Accelerometer extends VirtualSubsystem { private static final int PROFILE_EVERY_N = 50; // 1Hz profiling public Accelerometer(Imu imu) { + this(imu, new RioAccelIORoboRIO(SensorConstants.kRioAccelerometerSampleRateHz)); + } + + public Accelerometer(Imu imu, RioAccelIO rio) { this.imu = imu; - this.rio = new RioAccelIORoboRIO(200.0); // 200 Hz is a good start + this.rio = rio; } // Priority value for this virtual subsystem @@ -70,12 +75,15 @@ public void rbsiPeriodic() { // Compute RIO accelerations and jerks rawRio = new Translation3d( - rioInputs.xG * Constants.G_TO_MPS2, - rioInputs.yG * Constants.G_TO_MPS2, - rioInputs.zG * Constants.G_TO_MPS2); + rioInputs.xG * Constants.kGravityMetersPerSecSq, + rioInputs.yG * Constants.kGravityMetersPerSecSq, + rioInputs.zG * Constants.kGravityMetersPerSecSq); rioAcc = rawRio.rotateBy(RobotConstants.kRioOrientation); - // Acceleration from previous loop + Translation3d rioJerkThisLoop = + prevRioAcc == null + ? Translation3d.kZero + : rioAcc.minus(prevRioAcc).div(Constants.kLoopPeriodSecs); prevRioAcc = rioAcc; // IMU accelerations and jerks @@ -89,7 +97,7 @@ public void rbsiPeriodic() { final boolean doHeavyLogs = (++loopCount >= LOG_EVERY_N); if (doHeavyLogs) { loopCount = 0; - rioJerk = rioAcc.minus(prevRioAcc).div(Constants.loopPeriodSecs); + rioJerk = rioJerkThisLoop; imuJerk = imuInputs.linearJerk.rotateBy(RobotConstants.kIMUOrientation); Logger.recordOutput("Accel/Rio/Jerk_mps3", rioJerk); Logger.recordOutput("Accel/IMU/Jerk_mps3", imuJerk); diff --git a/src/main/java/frc/robot/subsystems/accelerometer/RioAccelIO.java b/src/main/java/frc/robot/subsystems/accelerometer/RioAccelIO.java index 64fad53a..2a60a893 100644 --- a/src/main/java/frc/robot/subsystems/accelerometer/RioAccelIO.java +++ b/src/main/java/frc/robot/subsystems/accelerometer/RioAccelIO.java @@ -23,4 +23,17 @@ final class Inputs { default void updateInputs(Inputs inputs) {} default void close() {} + + static RioAccelIO noop() { + return new RioAccelIO() { + @Override + public void updateInputs(Inputs inputs) { + inputs.connected = false; + inputs.timestampNs = 0L; + inputs.xG = 0.0; + inputs.yG = 0.0; + inputs.zG = 0.0; + } + }; + } } diff --git a/src/main/java/frc/robot/subsystems/drive/Drive.java b/src/main/java/frc/robot/subsystems/drive/Drive.java index c6e36a5a..ea54345d 100644 --- a/src/main/java/frc/robot/subsystems/drive/Drive.java +++ b/src/main/java/frc/robot/subsystems/drive/Drive.java @@ -21,7 +21,6 @@ import static frc.robot.subsystems.drive.SwerveConstants.*; import choreo.trajectory.SwerveSample; -import com.ctre.phoenix6.swerve.SwerveRequest; import com.pathplanner.lib.auto.AutoBuilder; import com.pathplanner.lib.config.PIDConstants; import com.pathplanner.lib.controllers.PPHolonomicDriveController; @@ -30,6 +29,7 @@ import edu.wpi.first.hal.FRCNetComm.tInstances; import edu.wpi.first.hal.FRCNetComm.tResourceType; import edu.wpi.first.hal.HAL; +import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.controller.PIDController; import edu.wpi.first.math.controller.ProfiledPIDController; import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; @@ -45,6 +45,8 @@ import edu.wpi.first.wpilibj.Alert.AlertType; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.DriverStation.Alliance; +import edu.wpi.first.wpilibj.smartdashboard.Field2d; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.Constants; @@ -81,11 +83,13 @@ public class Drive extends RBSISubsystem { // Pose Buffer Declarations private final ConcurrentTimeInterpolatableBuffer poseBuffer = - ConcurrentTimeInterpolatableBuffer.createBuffer(DrivebaseConstants.kHistorySize); + ConcurrentTimeInterpolatableBuffer.createBuffer(DrivebaseConstants.kPoseBufferHistorySecs); private final ConcurrentTimeInterpolatableBuffer yawBuffer = - ConcurrentTimeInterpolatableBuffer.createDoubleBuffer(DrivebaseConstants.kHistorySize); + ConcurrentTimeInterpolatableBuffer.createDoubleBuffer( + DrivebaseConstants.kPoseBufferHistorySecs); private final ConcurrentTimeInterpolatableBuffer yawRateBuffer = - ConcurrentTimeInterpolatableBuffer.createDoubleBuffer(DrivebaseConstants.kHistorySize); + ConcurrentTimeInterpolatableBuffer.createDoubleBuffer( + DrivebaseConstants.kPoseBufferHistorySecs); // Declare an alert private final Alert gyroDisconnectedAlert = @@ -109,6 +113,7 @@ public class Drive extends RBSISubsystem { // Declare PID controller and siumulation physics private ProfiledPIDController angleController; private DriveSimPhysics simPhysics; + private final Field2d field = new Field2d(); // Pose reset gate (vision + anything latency-sensitive) private volatile long poseResetEpoch = 0; // monotonic counter @@ -134,12 +139,13 @@ public Drive(Imu imu) { // Define the Angle Controller angleController = new ProfiledPIDController( - DrivebaseConstants.kPSPin, - DrivebaseConstants.kISPin, - DrivebaseConstants.kDSpin, + DrivebaseConstants.kSpinP, + DrivebaseConstants.kSpinI, + DrivebaseConstants.kSpinD, new TrapezoidProfile.Constraints( - getMaxAngularSpeedRadPerSec(), getMaxLinearAccelMetersPerSecPerSec())); + getMaxAngularSpeedRadPerSec(), getMaxAngularAccelRadPerSecPerSec())); angleController.enableContinuousInput(-Math.PI, Math.PI); + m_pathThetaController.enableContinuousInput(-Math.PI, Math.PI); // If REAL (i.e., NOT simulation), parse out the module types if (Constants.getMode() == Mode.REAL) { @@ -165,6 +171,7 @@ public Drive(Imu imu) { throw new RuntimeException( "For an all-CTRE drive base, use Phoenix Tuner X Swerve Generator instead of YAGSL!"); } + break; case 0b00010000: // Blended Talon Drive / NEO Steer modules[i] = new Module(new ModuleIOBlended(i), i); break; @@ -186,6 +193,7 @@ public Drive(Imu imu) { // Start odometry thread (for the real robot) PhoenixOdometryThread.getInstance().start(); + SparkOdometryThread.getInstance().start(); } else { @@ -198,8 +206,8 @@ public Drive(Imu imu) { simPhysics = new DriveSimPhysics( kinematics, - RobotConstants.kRobotMOI, // kg m^2 - RobotConstants.kMaxWheelTorque); // Nm + RobotConstants.kMomentOfInertiaKgMetersSq, // kg m^2 + RobotConstants.kMaxWheelTorqueNm); // Nm } // Usage reporting for swerve template @@ -217,13 +225,13 @@ public Drive(Imu imu) { (speeds, feedforwards) -> runVelocity(speeds), new PPHolonomicDriveController( new PIDConstants( - DrivebaseConstants.kPStrafe, - DrivebaseConstants.kIStrafe, - DrivebaseConstants.kDStrafe), + DrivebaseConstants.kStrafeP, + DrivebaseConstants.kStrafeI, + DrivebaseConstants.kStrafeD), new PIDConstants( - DrivebaseConstants.kPSPin, - DrivebaseConstants.kISPin, - DrivebaseConstants.kDSpin)), + DrivebaseConstants.kSpinP, + DrivebaseConstants.kSpinI, + DrivebaseConstants.kSpinD)), AutoConstants.kPathPlannerConfig, () -> DriverStation.getAlliance().orElse(Alliance.Blue) == Alliance.Red, this); @@ -243,7 +251,7 @@ public Drive(Imu imu) { break; case CHOREO: - // TODO: If your team is using Choreo, you'll know what to do here... + // Choreo autos are configured in RobotContainer through AutoFactory. break; case MANUAL: @@ -262,6 +270,8 @@ public Drive(Imu imu) { (state) -> Logger.recordOutput("Drive/SysIdState", state.toString())), new SysIdRoutine.Mechanism( (voltage) -> runCharacterization(voltage.in(Volts)), null, this)); + + SmartDashboard.putData("Field", field); } /************************************************************************* */ @@ -276,6 +286,8 @@ public void rbsiPeriodic() { Logger.recordOutput("SwerveStates/Setpoints", new SwerveModuleState[] {}); Logger.recordOutput("SwerveStates/SetpointsOptimized", new SwerveModuleState[] {}); } + + field.setRobotPose(m_PoseEstimator.getEstimatedPosition()); } /** @@ -291,7 +303,7 @@ public void simulationPeriodic() { // IMPORTANT: do not run sim physics during REPLAY if (Constants.getMode() != Mode.SIM) return; - final double dt = Constants.loopPeriodSecs; + final double dt = Constants.kLoopPeriodSecs; // Advance module wheel physics for (int i = 0; i < modules.length; i++) { @@ -366,7 +378,7 @@ public void stopWithX() { */ public void runVelocity(ChassisSpeeds speeds) { // Calculate module setpoints - ChassisSpeeds discreteSpeeds = ChassisSpeeds.discretize(speeds, Constants.loopPeriodSecs); + ChassisSpeeds discreteSpeeds = ChassisSpeeds.discretize(speeds, Constants.kLoopPeriodSecs); SwerveModuleState[] setpointStates = kinematics.toSwerveModuleStates(discreteSpeeds); SwerveDriveKinematics.desaturateWheelSpeeds(setpointStates, getMaxLinearSpeedMetersPerSec()); @@ -425,8 +437,8 @@ public void updateDisabledCoastState( SwerveModulePosition[] odomPositions) { // Don’t end coast “instantly” right after disable edge - final double minCoastTime = 0.25; // seconds -- maybe put into Constants??? - final boolean pastMin = (now - disabledCoastStartTs) >= minCoastTime; + final boolean pastMin = + (now - disabledCoastStartTs) >= DrivebaseConstants.kDisabledCoastMinSeconds; // Detect ENABLED -> DISABLED edge -- set `disabledCoastUntilTs` when COAST-phase ends if (lastEnabled && !enabledNow) { @@ -455,8 +467,10 @@ public void updateDisabledCoastState( return; } - // Compute max wheel delta this loop + // Compute max wheel delta this loop. The first sample after a reset only establishes the + // baseline and should not count as stationary. double maxDelta = 0.0; + boolean hadLastWheelDist = haveLastWheelDist; if (haveLastWheelDist) { for (int i = 0; i < 4; i++) { double dist = odomPositions[i].distanceMeters; @@ -472,7 +486,7 @@ public void updateDisabledCoastState( haveLastWheelDist = true; // Stationary test (must have baseline) - if (haveLastWheelDist + if (hadLastWheelDist && maxDelta <= DrivebaseConstants.kStationaryMaxWheelDeltaM && Math.abs(yawRateRadPerSec) <= DrivebaseConstants.kStationaryMaxYawRateRadPerSec) { stationaryLoops++; @@ -499,13 +513,15 @@ public void updateDisabledCoastState( /** Returns a command to run a quasistatic test in the specified direction. */ public Command sysIdQuasistatic(SysIdRoutine.Direction direction) { return run(() -> runCharacterization(0.0)) - .withTimeout(1.0) + .withTimeout(DrivebaseConstants.kSysIdPreRunStopSecs) .andThen(sysId.quasistatic(direction)); } /** Returns a command to run a dynamic test in the specified direction. */ public Command sysIdDynamic(SysIdRoutine.Direction direction) { - return run(() -> runCharacterization(0.0)).withTimeout(1.0).andThen(sysId.dynamic(direction)); + return run(() -> runCharacterization(0.0)) + .withTimeout(DrivebaseConstants.kSysIdPreRunStopSecs) + .andThen(sysId.dynamic(direction)); } /************************************************************************* */ @@ -600,12 +616,12 @@ public Optional getPoseAtTime(double timestampSeconds) { /** Returns the oldest timetamp in the current pose buffer */ public double getPoseBufferOldestTime() { - return poseBuffer.getOldestTimestamp().getAsDouble(); + return poseBuffer.getOldestTimestamp().orElse(Double.NaN); } /** Returns the newest timetamp in the current pose buffer */ public double getPoseBufferNewestTime() { - return poseBuffer.getNewestTimestamp().getAsDouble(); + return poseBuffer.getNewestTimestamp().orElse(Double.NaN); } /** @@ -620,7 +636,7 @@ public OptionalDouble getMaxAbsYawRateRadPerSec(double t0, double t1) { if (t1 < t0) return OptionalDouble.empty(); // Get the subset of entries from the buffer - var sub = yawRateBuffer.getInternalBuffer().subMap(t0, true, t1, true); + var sub = yawRateBuffer.getSamplesInRange(t0, true, t1, true); if (sub.isEmpty()) return OptionalDouble.empty(); double maxAbs = 0.0; @@ -646,7 +662,7 @@ public double getLastPoseResetTimestamp() { /** Returns the maximum linear speed in meters per sec. */ public double getMaxLinearSpeedMetersPerSec() { - return DrivebaseConstants.kMaxLinearSpeed; + return DrivebaseConstants.kMaxLinearSpeedMetersPerSec; } /** Returns the maximum angular speed in radians per sec. */ @@ -656,7 +672,7 @@ public double getMaxAngularSpeedRadPerSec() { /** Returns the maximum linear acceleration in meters per sec per sec. */ public double getMaxLinearAccelMetersPerSecPerSec() { - return DrivebaseConstants.kMaxLinearAccel; + return DrivebaseConstants.kMaxLinearAccelMetersPerSecSq; } /** Returns the maximum angular acceleration in radians per sec per sec */ @@ -778,7 +794,9 @@ public void addVisionMeasurement(TimedPose meas) { // If we're coasting, avoid snapping Pose to Vision; lean gentler than stationary. final double alpha = coast - ? Math.min(DrivebaseConstants.kDisabledVisionBlendAlpha, 0.05) + ? Math.min( + DrivebaseConstants.kDisabledVisionBlendAlpha, + DrivebaseConstants.kDisabledVisionCoastBlendAlpha) : DrivebaseConstants.kDisabledVisionBlendAlpha; // "Current" for blending target (estimator pose) @@ -912,7 +930,7 @@ void yawBuffersFillFromQueue(double[] yawTs, double[] yawPosRad) { if (k > 0) { double dt = yawTs[k] - yawTs[k - 1]; if (dt > 1e-6) { - yawRateBuffer.addSample(yawTs[k], (yawPosRad[k] - yawPosRad[k - 1]) / dt); + yawRateBuffer.addSample(yawTs[k], yawRateRadPerSec(yawPosRad[k - 1], yawPosRad[k], dt)); } } } @@ -924,11 +942,16 @@ void yawBuffersAddSampleIndexAligned(double t, double[] yawTs, double[] yawPos, if (i > 0) { double dt = yawTs[i] - yawTs[i - 1]; if (dt > 1e-6) { - yawRateBuffer.addSample(t, (yawPos[i] - yawPos[i - 1]) / dt); + yawRateBuffer.addSample(t, yawRateRadPerSec(yawPos[i - 1], yawPos[i], dt)); } } } + static double yawRateRadPerSec(double previousYawRad, double currentYawRad, double dtSec) { + if (dtSec <= 1e-6) return 0.0; + return MathUtil.angleModulus(currentYawRad - previousYawRad) / dtSec; + } + /** Set the gyroDisconnectedAlert */ void setGyroDisconnectedAlert(boolean disconnected) { gyroDisconnectedAlert.set(disconnected); @@ -952,20 +975,26 @@ public double getSimYawRateRadPerSec() { /** CHOREO SECTION (Ignore if AutoType == PATHPLANNER) ******************* */ /** Choreo: Reset odometry */ - public Command resetOdometry(Pose2d orElseGet) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'resetOdometry'"); + public void resetOdometry(Pose2d pose) { + resetPose(pose); } - /** Swerve request to apply during field-centric path following */ - @SuppressWarnings("unused") - private final SwerveRequest.ApplyFieldSpeeds m_pathApplyFieldSpeeds = - new SwerveRequest.ApplyFieldSpeeds(); - // Choreo Controller Values - private final PIDController m_pathXController = new PIDController(10, 0, 0); - private final PIDController m_pathYController = new PIDController(10, 0, 0); - private final PIDController m_pathThetaController = new PIDController(7, 0, 0); + private final PIDController m_pathXController = + new PIDController( + AutoConstants.kChoreoDrivePID.kP, + AutoConstants.kChoreoDrivePID.kI, + AutoConstants.kChoreoDrivePID.kD); + private final PIDController m_pathYController = + new PIDController( + AutoConstants.kChoreoDrivePID.kP, + AutoConstants.kChoreoDrivePID.kI, + AutoConstants.kChoreoDrivePID.kD); + private final PIDController m_pathThetaController = + new PIDController( + AutoConstants.kChoreoSteerPID.kP, + AutoConstants.kChoreoSteerPID.kI, + AutoConstants.kChoreoSteerPID.kD); /** * Follows the given field-centric path sample with PID for Choreo @@ -974,34 +1003,28 @@ public Command resetOdometry(Pose2d orElseGet) { * @param sample Sample along the path to follow */ public void choreoController(Pose2d pose, SwerveSample sample) { - m_pathThetaController.enableContinuousInput(-Math.PI, Math.PI); - var targetSpeeds = sample.getChassisSpeeds(); targetSpeeds.vxMetersPerSecond += m_pathXController.calculate(pose.getX(), sample.x); targetSpeeds.vyMetersPerSecond += m_pathYController.calculate(pose.getY(), sample.y); targetSpeeds.omegaRadiansPerSecond += m_pathThetaController.calculate(pose.getRotation().getRadians(), sample.heading); - // setControl( - // m_pathApplyFieldSpeeds - // .withSpeeds(targetSpeeds) - // .withWheelForceFeedforwardsX(sample.moduleForcesX()) - // .withWheelForceFeedforwardsY(sample.moduleForcesY())); + runVelocity(ChassisSpeeds.fromFieldRelativeSpeeds(targetSpeeds, getHeading())); } public void followTrajectory(SwerveSample sample) { // Get the current pose of the robot Pose2d pose = getPose(); - // Generate the next speeds for the robot - ChassisSpeeds speeds = + // Choreo samples are field-relative; convert to robot-relative before sending to modules. + ChassisSpeeds fieldRelativeSpeeds = new ChassisSpeeds( sample.vx + m_pathXController.calculate(pose.getX(), sample.x), - sample.vy + m_pathXController.calculate(pose.getX(), sample.y), + sample.vy + m_pathYController.calculate(pose.getY(), sample.y), sample.omega - + m_pathXController.calculate(pose.getRotation().getRadians(), sample.heading)); + + m_pathThetaController.calculate(pose.getRotation().getRadians(), sample.heading)); // Apply the generated speeds - runVelocity(speeds); + runVelocity(ChassisSpeeds.fromFieldRelativeSpeeds(fieldRelativeSpeeds, pose.getRotation())); } } diff --git a/src/main/java/frc/robot/subsystems/drive/DriveOdometry.java b/src/main/java/frc/robot/subsystems/drive/DriveOdometry.java index 7dc4925b..6f4b49bb 100644 --- a/src/main/java/frc/robot/subsystems/drive/DriveOdometry.java +++ b/src/main/java/frc/robot/subsystems/drive/DriveOdometry.java @@ -187,6 +187,7 @@ public void rbsiPeriodic() { // ---------------------------------------------------------------------- final double[] lastDist = new double[4]; boolean haveLastDist = false; + final boolean logDebug = Constants.isTuningMode(); for (int i = 0; i < n; i++) { final double t = ts[i]; @@ -224,26 +225,31 @@ public void rbsiPeriodic() { imuInputs.yawRateRadPerSec, odomPositions); - // Debugging - Logger.recordOutput("Odometry/Debug/timestamp", t); - Logger.recordOutput("Odometry/Debug/now", TimeUtil.now()); - if (i > 0) { - Logger.recordOutput("Odometry/Debug/timeNowDiff", t - ts[i - 1]); + if (logDebug && i == n - 1) { + Logger.recordOutput("Odometry/Debug/timestamp", t); + Logger.recordOutput("Odometry/Debug/now", TimeUtil.now()); + if (i > 0) { + Logger.recordOutput("Odometry/Debug/timeNowDiff", t - ts[i - 1]); + } + Logger.recordOutput("Odometry/Debug/replay_t", t); + Logger.recordOutput("Odometry/Debug/replay_yawRad", yawRad); } - Logger.recordOutput("Odometry/Debug/replay_t", t); - Logger.recordOutput("Odometry/Debug/replay_yawRad", yawRad); // Module distance deltas (valid within batch) for (int m = 0; m < 4; m++) { final SwerveModulePosition pos = odomPositions[m]; final double dist = pos.distanceMeters; - Logger.recordOutput("Odometry/Debug/mod" + m + "_distanceMeters", dist); - Logger.recordOutput("Odometry/Debug/mod" + m + "_angleRad", pos.angle.getRadians()); + if (logDebug && i == n - 1) { + Logger.recordOutput("Odometry/Debug/mod" + m + "_distanceMeters", dist); + Logger.recordOutput("Odometry/Debug/mod" + m + "_angleRad", pos.angle.getRadians()); + } if (haveLastDist) { final double delta = dist - lastDist[m]; - Logger.recordOutput("Odometry/Debug/mod" + m + "_deltaMeters", delta); + if (logDebug && i == n - 1) { + Logger.recordOutput("Odometry/Debug/mod" + m + "_deltaMeters", delta); + } } lastDist[m] = dist; diff --git a/src/main/java/frc/robot/subsystems/drive/Module.java b/src/main/java/frc/robot/subsystems/drive/Module.java index b57653d8..87db48e6 100644 --- a/src/main/java/frc/robot/subsystems/drive/Module.java +++ b/src/main/java/frc/robot/subsystems/drive/Module.java @@ -25,6 +25,8 @@ * the robot. This is not a true subsystem, but an abstraction layer. */ public class Module { + private static final SwerveModulePosition[] EMPTY_ODOMETRY_POSITIONS = + new SwerveModulePosition[0]; // Define IO private final ModuleIO io; @@ -35,7 +37,7 @@ public class Module { private final Alert driveDisconnectedAlert; private final Alert turnDisconnectedAlert; private final Alert turnEncoderDisconnectedAlert; - private SwerveModulePosition[] odometryPositions = new SwerveModulePosition[] {}; + private SwerveModulePosition[] odometryPositions = EMPTY_ODOMETRY_POSITIONS; /** Constructor */ public Module(ModuleIO io, int index) { @@ -61,7 +63,17 @@ public void periodic() { Logger.processInputs("Drive/Module" + Integer.toString(index), inputs); // Calculate positions for odometry - int sampleCount = inputs.odometryTimestamps.length; // All signals are sampled together + int sampleCount = + Math.min( + inputs.odometryTimestamps.length, + Math.min(inputs.odometryDrivePositionsRad.length, inputs.odometryTurnPositions.length)); + if (sampleCount <= 0) { + odometryPositions = EMPTY_ODOMETRY_POSITIONS; + driveDisconnectedAlert.set(!inputs.driveConnected); + turnDisconnectedAlert.set(!inputs.turnConnected); + turnEncoderDisconnectedAlert.set(!inputs.turnEncoderConnected); + return; + } odometryPositions = new SwerveModulePosition[sampleCount]; for (int i = 0; i < sampleCount; i++) { double positionMeters = diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIO.java b/src/main/java/frc/robot/subsystems/drive/ModuleIO.java index 10055776..7affb4f5 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIO.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIO.java @@ -13,6 +13,9 @@ import org.littletonrobotics.junction.AutoLog; public interface ModuleIO { + double[] EMPTY_DOUBLE_ARRAY = new double[0]; + Rotation2d[] EMPTY_ROTATION_ARRAY = new Rotation2d[0]; + @AutoLog public static class ModuleIOInputs { public boolean driveConnected = false; @@ -29,9 +32,9 @@ public static class ModuleIOInputs { public double turnAppliedVolts = 0.0; public double turnCurrentAmps = 0.0; - public double[] odometryTimestamps = new double[] {}; - public double[] odometryDrivePositionsRad = new double[] {}; - public Rotation2d[] odometryTurnPositions = new Rotation2d[] {}; + public double[] odometryTimestamps = EMPTY_DOUBLE_ARRAY; + public double[] odometryDrivePositionsRad = EMPTY_DOUBLE_ARRAY; + public Rotation2d[] odometryTurnPositions = EMPTY_ROTATION_ARRAY; } /** Updates the set of loggable inputs. */ diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOBlended.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOBlended.java index 9ae8105b..8ddb7138 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOBlended.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOBlended.java @@ -19,7 +19,6 @@ import com.ctre.phoenix6.configs.OpenLoopRampsConfigs; import com.ctre.phoenix6.configs.Slot0Configs; import com.ctre.phoenix6.configs.TalonFXConfiguration; -import com.ctre.phoenix6.controls.TorqueCurrentFOC; import com.ctre.phoenix6.controls.VelocityTorqueCurrentFOC; import com.ctre.phoenix6.controls.VelocityVoltage; import com.ctre.phoenix6.controls.VoltageOut; @@ -95,7 +94,6 @@ public class ModuleIOBlended implements ModuleIO { private final VelocityVoltage velocityVoltageRequest = new VelocityVoltage(0.0); // Torque-current control requests - private final TorqueCurrentFOC torqueCurrentRequest = new TorqueCurrentFOC(0); private final VelocityTorqueCurrentFOC velocityTorqueCurrentRequest = new VelocityTorqueCurrentFOC(0.0); @@ -208,19 +206,19 @@ public ModuleIOBlended(int module) { .withKV(DrivebaseConstants.kDriveV) .withKA(DrivebaseConstants.kDriveA); driveConfig.Feedback.SensorToMechanismRatio = SwerveConstants.kDriveGearRatio; - driveConfig.TorqueCurrent.PeakForwardTorqueCurrent = DrivebaseConstants.kSlipCurrent; - driveConfig.TorqueCurrent.PeakReverseTorqueCurrent = -DrivebaseConstants.kSlipCurrent; + driveConfig.TorqueCurrent.PeakForwardTorqueCurrent = DrivebaseConstants.kSlipCurrentAmps; + driveConfig.TorqueCurrent.PeakReverseTorqueCurrent = -DrivebaseConstants.kSlipCurrentAmps; driveConfig.CurrentLimits.StatorCurrentLimit = SwerveConstants.kDriveCurrentLimit; driveConfig.CurrentLimits.StatorCurrentLimitEnable = true; // Build the OpenLoopRampsConfigs and ClosedLoopRampsConfigs for current smoothing OpenLoopRampsConfigs openRamps = new OpenLoopRampsConfigs(); - openRamps.DutyCycleOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; - openRamps.VoltageOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; - openRamps.TorqueOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; + openRamps.DutyCycleOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; + openRamps.VoltageOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; + openRamps.TorqueOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; ClosedLoopRampsConfigs closedRamps = new ClosedLoopRampsConfigs(); - closedRamps.DutyCycleClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; - closedRamps.VoltageClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; - closedRamps.TorqueClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; + closedRamps.DutyCycleClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; + closedRamps.VoltageClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; + closedRamps.TorqueClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; // Apply the open- and closed-loop ramp configuration for current smoothing driveConfig.withClosedLoopRamps(closedRamps).withOpenLoopRamps(openRamps); // Set motor inversions @@ -234,7 +232,7 @@ public ModuleIOBlended(int module) { .inverted(constants.SteerMotorInverted) .idleMode(IdleMode.kBrake) .smartCurrentLimit((int) SwerveConstants.kSteerCurrentLimit) - .voltageCompensation(DrivebaseConstants.kOptimalVoltage); + .voltageCompensation(DrivebaseConstants.kNominalVoltage); turnConfig .absoluteEncoder .inverted(constants.EncoderInverted) @@ -256,13 +254,13 @@ public ModuleIOBlended(int module) { .absoluteEncoderPositionAlwaysOn(true) .absoluteEncoderPositionPeriodMs((int) (1000.0 / SwerveConstants.kOdometryFrequency)) .absoluteEncoderVelocityAlwaysOn(true) - .absoluteEncoderVelocityPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .appliedOutputPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .busVoltagePeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .outputCurrentPeriodMs((int) (Constants.loopPeriodSecs * 1000.)); + .absoluteEncoderVelocityPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .appliedOutputPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .busVoltagePeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .outputCurrentPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)); turnConfig - .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriod) - .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriod); + .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriodSecs) + .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriodSecs); // Configure CANCoder CANcoderConfiguration cancoderConfig = constants.EncoderInitialConfigs; @@ -354,9 +352,9 @@ public void updateInputs(ModuleIOInputs inputs) { final int sampleCount = Math.min(tsCount, Math.min(driveCount, turnCount)); if (sampleCount <= 0) { - inputs.odometryTimestamps = new double[0]; - inputs.odometryDrivePositionsRad = new double[0]; - inputs.odometryTurnPositions = new Rotation2d[0]; + inputs.odometryTimestamps = EMPTY_DOUBLE_ARRAY; + inputs.odometryDrivePositionsRad = EMPTY_DOUBLE_ARRAY; + inputs.odometryTurnPositions = EMPTY_ROTATION_ARRAY; return; } @@ -395,13 +393,9 @@ public void updateInputs(ModuleIOInputs inputs) { public void setDriveOpenLoop(double output) { // Scale by actual battery voltage to keep full output consistent double busVoltage = RobotController.getBatteryVoltage(); - double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; - driveTalon.setControl( - switch (m_DriveMotorClosedLoopOutput) { - case Voltage -> voltageRequest.withOutput(scaledOutput); - case TorqueCurrentFOC -> torqueCurrentRequest.withOutput(scaledOutput); - }); + driveTalon.setControl(voltageRequest.withOutput(scaledOutput)); // Log output and battery Logger.recordOutput("Swerve/Drive/OpenLoopOutput", scaledOutput); @@ -416,7 +410,7 @@ public void setDriveOpenLoop(double output) { @Override public void setTurnOpenLoop(double output) { double busVoltage = RobotController.getBatteryVoltage(); - double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; turnSpark.setVoltage(scaledOutput); // Log output and battery @@ -448,7 +442,7 @@ public void setDriveVelocity(double velocityRadPerSec) { Math.signum(velocityRotPerSec) * DrivebaseConstants.kDriveS + DrivebaseConstants.kDriveV * velocityRotPerSec + DrivebaseConstants.kDriveA * accelerationRotPerSec2; - double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; // Set the drive motor control based on CTRE LICENSED status driveTalon.setControl( diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java index ec87f59e..740d42e2 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOSim.java @@ -75,7 +75,7 @@ public void simulationPeriodic() { } if (turnClosedLoop) { - // TODO: turn PID has no feedforward or inertia compensation; fix + // Simple angle loop for module simulation; real feedforward is handled by hardware IO. turnAppliedVolts = turnController.calculate(turnSim.getAngularPositionRad()); } else { turnController.reset(); @@ -86,8 +86,8 @@ public void simulationPeriodic() { turnSim.setInputVoltage(MathUtil.clamp(turnAppliedVolts, -12.0, 12.0)); // Advance physics - driveSim.update(Constants.loopPeriodSecs); - turnSim.update(Constants.loopPeriodSecs); + driveSim.update(Constants.kLoopPeriodSecs); + turnSim.update(Constants.kLoopPeriodSecs); } @Override diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOSpark.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOSpark.java index c79d5ee9..0fc8c2cc 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOSpark.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOSpark.java @@ -129,7 +129,7 @@ public ModuleIOSpark(int module) { driveConfig .idleMode(IdleMode.kBrake) .smartCurrentLimit((int) SwerveConstants.kDriveCurrentLimit) - .voltageCompensation(DrivebaseConstants.kOptimalVoltage); + .voltageCompensation(DrivebaseConstants.kNominalVoltage); driveConfig .encoder .positionConversionFactor(SwerveConstants.driveEncoderPositionFactor) @@ -148,13 +148,13 @@ public ModuleIOSpark(int module) { .primaryEncoderPositionAlwaysOn(true) .primaryEncoderPositionPeriodMs((int) (1000.0 / SwerveConstants.kOdometryFrequency)) .primaryEncoderVelocityAlwaysOn(true) - .primaryEncoderVelocityPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .appliedOutputPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .busVoltagePeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .outputCurrentPeriodMs((int) (Constants.loopPeriodSecs * 1000.)); + .primaryEncoderVelocityPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .appliedOutputPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .busVoltagePeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .outputCurrentPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)); driveConfig - .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriod) - .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriod); + .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriodSecs) + .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriodSecs); SparkUtil.tryUntilOk( driveSpark, 5, @@ -169,7 +169,7 @@ public ModuleIOSpark(int module) { .inverted(turnInverted) .idleMode(IdleMode.kBrake) .smartCurrentLimit((int) SwerveConstants.kSteerCurrentLimit) - .voltageCompensation(DrivebaseConstants.kOptimalVoltage); + .voltageCompensation(DrivebaseConstants.kNominalVoltage); turnConfig .absoluteEncoder .inverted(turnEncoderInverted) @@ -191,13 +191,13 @@ public ModuleIOSpark(int module) { .absoluteEncoderPositionAlwaysOn(true) .absoluteEncoderPositionPeriodMs((int) (1000.0 / SwerveConstants.kOdometryFrequency)) .absoluteEncoderVelocityAlwaysOn(true) - .absoluteEncoderVelocityPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .appliedOutputPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .busVoltagePeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .outputCurrentPeriodMs((int) (Constants.loopPeriodSecs * 1000.)); + .absoluteEncoderVelocityPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .appliedOutputPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .busVoltagePeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .outputCurrentPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)); turnConfig - .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriod) - .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriod); + .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriodSecs) + .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriodSecs); SparkUtil.tryUntilOk( turnSpark, 5, @@ -256,9 +256,9 @@ public void updateInputs(ModuleIOInputs inputs) { final int sampleCount = Math.min(tsCount, Math.min(driveCount, turnCount)); if (sampleCount <= 0) { - inputs.odometryTimestamps = new double[0]; - inputs.odometryDrivePositionsRad = new double[0]; - inputs.odometryTurnPositions = new Rotation2d[0]; + inputs.odometryTimestamps = EMPTY_DOUBLE_ARRAY; + inputs.odometryDrivePositionsRad = EMPTY_DOUBLE_ARRAY; + inputs.odometryTurnPositions = EMPTY_ROTATION_ARRAY; return; } @@ -337,7 +337,7 @@ public void setDriveVelocity(double velocityRadPerSec) { + DrivebaseConstants.kDriveA * accelerationRadPerSec2; double busVoltage = RobotController.getBatteryVoltage(); - double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; driveController.setSetpoint( velocityRadPerSec, diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkCANcoder.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkCANcoder.java index 7c106838..743bb495 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkCANcoder.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOSparkCANcoder.java @@ -158,7 +158,7 @@ public ModuleIOSparkCANcoder(int module) { driveConfig .idleMode(IdleMode.kBrake) .smartCurrentLimit((int) SwerveConstants.kDriveCurrentLimit) - .voltageCompensation(DrivebaseConstants.kOptimalVoltage); + .voltageCompensation(DrivebaseConstants.kNominalVoltage); driveConfig .encoder .positionConversionFactor(SwerveConstants.driveEncoderPositionFactor) @@ -195,7 +195,7 @@ public ModuleIOSparkCANcoder(int module) { .inverted(turnInverted) .idleMode(IdleMode.kBrake) .smartCurrentLimit((int) SwerveConstants.kSteerCurrentLimit) - .voltageCompensation(DrivebaseConstants.kOptimalVoltage); + .voltageCompensation(DrivebaseConstants.kNominalVoltage); turnConfig .absoluteEncoder .inverted(turnEncoderInverted) @@ -217,13 +217,13 @@ public ModuleIOSparkCANcoder(int module) { .absoluteEncoderPositionAlwaysOn(true) .absoluteEncoderPositionPeriodMs((int) (1000.0 / SwerveConstants.kOdometryFrequency)) .absoluteEncoderVelocityAlwaysOn(true) - .absoluteEncoderVelocityPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .appliedOutputPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .busVoltagePeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .outputCurrentPeriodMs((int) (Constants.loopPeriodSecs * 1000.)); + .absoluteEncoderVelocityPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .appliedOutputPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .busVoltagePeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .outputCurrentPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)); turnConfig - .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriod) - .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriod); + .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriodSecs) + .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriodSecs); SparkUtil.tryUntilOk( turnSpark, 5, @@ -235,18 +235,25 @@ public ModuleIOSparkCANcoder(int module) { turnVelocity = cancoder.getVelocity(); turnAbsolutePosition = cancoder.getAbsolutePosition(); turnPosition = cancoder.getPosition(); + BaseStatusSignal.setUpdateFrequencyForAll(SwerveConstants.kOdometryFrequency, turnPosition); // Create odometry queues timestampQueue = SparkOdometryThread.getInstance().makeTimestampQueue(); drivePositionQueue = SparkOdometryThread.getInstance().registerSignal(driveSpark, driveEncoder::getPosition); - turnPositionQueue = PhoenixOdometryThread.getInstance().registerSignal(cancoder.getPosition()); + turnPositionQueue = + SparkOdometryThread.getInstance() + .registerSignal( + () -> { + turnPosition.refresh(); + return turnPosition.getValueAsDouble(); + }); } @Override public void updateInputs(ModuleIOInputs inputs) { // Refresh CANcoder absolute - var encStatus = BaseStatusSignal.refreshAll(turnAbsolutePosition); + var encStatus = BaseStatusSignal.refreshAll(turnAbsolutePosition, turnPosition, turnVelocity); if (!encStatus.isOK()) { Logger.recordOutput("CAN/Module" + module + "/EncRefreshStatus", encStatus.toString()); } @@ -286,9 +293,9 @@ public void updateInputs(ModuleIOInputs inputs) { final int sampleCount = Math.min(tsCount, Math.min(driveCount, turnCount)); if (sampleCount <= 0) { - inputs.odometryTimestamps = new double[0]; - inputs.odometryDrivePositionsRad = new double[0]; - inputs.odometryTurnPositions = new Rotation2d[0]; + inputs.odometryTimestamps = EMPTY_DOUBLE_ARRAY; + inputs.odometryDrivePositionsRad = EMPTY_DOUBLE_ARRAY; + inputs.odometryTurnPositions = EMPTY_ROTATION_ARRAY; return; } @@ -326,7 +333,7 @@ public void updateInputs(ModuleIOInputs inputs) { @Override public void setDriveOpenLoop(double output) { double busVoltage = RobotController.getBatteryVoltage(); - double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; driveSpark.setVoltage(scaledOutput); // Log output and battery @@ -342,7 +349,7 @@ public void setDriveOpenLoop(double output) { @Override public void setTurnOpenLoop(double output) { double busVoltage = RobotController.getBatteryVoltage(); - double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; turnSpark.setVoltage(scaledOutput); // Log output and battery @@ -373,7 +380,7 @@ public void setDriveVelocity(double velocityRadPerSec) { + DrivebaseConstants.kDriveA * accelerationRadPerSec2; double busVoltage = RobotController.getBatteryVoltage(); - double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; driveController.setSetpoint( velocityRadPerSec, diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFX.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFX.java index ccf9c270..802503bb 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFX.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFX.java @@ -21,7 +21,6 @@ import com.ctre.phoenix6.configs.TalonFXConfiguration; import com.ctre.phoenix6.controls.PositionTorqueCurrentFOC; import com.ctre.phoenix6.controls.PositionVoltage; -import com.ctre.phoenix6.controls.TorqueCurrentFOC; import com.ctre.phoenix6.controls.VelocityTorqueCurrentFOC; import com.ctre.phoenix6.controls.VelocityVoltage; import com.ctre.phoenix6.controls.VoltageOut; @@ -45,7 +44,7 @@ import edu.wpi.first.wpilibj.RobotController; import frc.robot.Constants; import frc.robot.Constants.DrivebaseConstants; -import frc.robot.generated.TunerConstants; +import frc.robot.generated.TunerFactory; import frc.robot.util.PhoenixUtil; import frc.robot.util.RBSICANBusRegistry; import java.util.Arrays; @@ -87,7 +86,6 @@ public class ModuleIOTalonFX implements ModuleIO { private final VelocityVoltage velocityVoltageRequest = new VelocityVoltage(0.0); // Torque-current control requests - private final TorqueCurrentFOC torqueCurrentRequest = new TorqueCurrentFOC(0); private final PositionTorqueCurrentFOC positionTorqueCurrentRequest = new PositionTorqueCurrentFOC(0.0); private final VelocityTorqueCurrentFOC velocityTorqueCurrentRequest = @@ -138,10 +136,10 @@ public ModuleIOTalonFX(int module) { constants = switch (module) { - case 0 -> TunerConstants.FrontLeft; - case 1 -> TunerConstants.FrontRight; - case 2 -> TunerConstants.BackLeft; - case 3 -> TunerConstants.BackRight; + case 0 -> TunerFactory.INSTANCE.frontLeft(); + case 1 -> TunerFactory.INSTANCE.frontRight(); + case 2 -> TunerFactory.INSTANCE.backLeft(); + case 3 -> TunerFactory.INSTANCE.backRight(); default -> throw new IllegalArgumentException("Invalid module index"); }; @@ -150,6 +148,8 @@ public ModuleIOTalonFX(int module) { turnTalon = new TalonFX(constants.SteerMotorId, canBus); cancoder = new CANcoder(constants.EncoderId, canBus); + Logger.recordOutput("Drive/EncoderOffsets/Module" + module, constants.EncoderOffset); + // Configure drive motor driveConfig.MotorOutput.NeutralMode = NeutralModeValue.Brake; driveConfig.Slot0 = @@ -161,19 +161,19 @@ public ModuleIOTalonFX(int module) { .withKV(DrivebaseConstants.kDriveV) .withKA(DrivebaseConstants.kDriveA); driveConfig.Feedback.SensorToMechanismRatio = SwerveConstants.kDriveGearRatio; - driveConfig.TorqueCurrent.PeakForwardTorqueCurrent = DrivebaseConstants.kSlipCurrent; - driveConfig.TorqueCurrent.PeakReverseTorqueCurrent = -DrivebaseConstants.kSlipCurrent; + driveConfig.TorqueCurrent.PeakForwardTorqueCurrent = DrivebaseConstants.kSlipCurrentAmps; + driveConfig.TorqueCurrent.PeakReverseTorqueCurrent = -DrivebaseConstants.kSlipCurrentAmps; driveConfig.CurrentLimits.StatorCurrentLimit = SwerveConstants.kDriveCurrentLimit; driveConfig.CurrentLimits.StatorCurrentLimitEnable = true; // Build the OpenLoopRampsConfigs and ClosedLoopRampsConfigs for current smoothing OpenLoopRampsConfigs openRamps = new OpenLoopRampsConfigs(); - openRamps.DutyCycleOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; - openRamps.VoltageOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; - openRamps.TorqueOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; + openRamps.DutyCycleOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; + openRamps.VoltageOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; + openRamps.TorqueOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; ClosedLoopRampsConfigs closedRamps = new ClosedLoopRampsConfigs(); - closedRamps.DutyCycleClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; - closedRamps.VoltageClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; - closedRamps.TorqueClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; + closedRamps.DutyCycleClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; + closedRamps.VoltageClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; + closedRamps.TorqueClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; // Apply the open- and closed-loop ramp configuration for current smoothing driveConfig.withClosedLoopRamps(closedRamps).withOpenLoopRamps(openRamps); // Set motor inversions @@ -219,7 +219,7 @@ public ModuleIOTalonFX(int module) { constants.EncoderInverted ? SensorDirectionValue.Clockwise_Positive : SensorDirectionValue.CounterClockwise_Positive; - cancoder.getConfigurator().apply(cancoderConfig); + PhoenixUtil.tryUntilOk(5, () -> cancoder.getConfigurator().apply(cancoderConfig)); // Create timestamp queue timestampQueue = PhoenixOdometryThread.getInstance().makeTimestampQueue(); @@ -310,9 +310,9 @@ public void updateInputs(ModuleIOInputs inputs) { final int sampleCount = Math.min(tsCount, Math.min(driveCount, turnCount)); if (sampleCount <= 0) { - inputs.odometryTimestamps = new double[0]; - inputs.odometryDrivePositionsRad = new double[0]; - inputs.odometryTurnPositions = new Rotation2d[0]; + inputs.odometryTimestamps = EMPTY_DOUBLE_ARRAY; + inputs.odometryDrivePositionsRad = EMPTY_DOUBLE_ARRAY; + inputs.odometryTurnPositions = EMPTY_ROTATION_ARRAY; return; } @@ -353,13 +353,9 @@ public void updateInputs(ModuleIOInputs inputs) { public void setDriveOpenLoop(double output) { // Scale by actual battery voltage to keep full output consistent double busVoltage = RobotController.getBatteryVoltage(); - double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; - driveTalon.setControl( - switch (m_DriveMotorClosedLoopOutput) { - case Voltage -> voltageRequest.withOutput(scaledOutput); - case TorqueCurrentFOC -> torqueCurrentRequest.withOutput(scaledOutput); - }); + driveTalon.setControl(voltageRequest.withOutput(scaledOutput)); // Log output and battery Logger.recordOutput("Swerve/Drive/OpenLoopOutput", scaledOutput); @@ -374,13 +370,9 @@ public void setDriveOpenLoop(double output) { @Override public void setTurnOpenLoop(double output) { double busVoltage = RobotController.getBatteryVoltage(); - double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; - turnTalon.setControl( - switch (m_SteerMotorClosedLoopOutput) { - case Voltage -> voltageRequest.withOutput(scaledOutput); - case TorqueCurrentFOC -> torqueCurrentRequest.withOutput(scaledOutput); - }); + turnTalon.setControl(voltageRequest.withOutput(scaledOutput)); // Log output and battery Logger.recordOutput("Swerve/Turn/OpenLoopOutput", scaledOutput); @@ -411,7 +403,7 @@ public void setDriveVelocity(double velocityRadPerSec) { Math.signum(velocityRotPerSec) * DrivebaseConstants.kDriveS + DrivebaseConstants.kDriveV * velocityRotPerSec + DrivebaseConstants.kDriveA * accelerationRotPerSec2; - double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; + double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; // Set the drive motor control based on CTRE LICENSED status driveTalon.setControl( @@ -441,8 +433,8 @@ public void setTurnPosition(Rotation2d rotation) { double busVoltage = RobotController.getBatteryVoltage(); // Scale feedforward voltage by battery voltage - double nominalFFVolts = DrivebaseConstants.kNominalFFVolts; - double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; + double nominalFFVolts = DrivebaseConstants.kNominalFeedforwardVolts; + double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; turnTalon.setControl( switch (m_SteerMotorClosedLoopOutput) { diff --git a/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFXS.java b/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFXS.java index 6226ed2f..bd618b43 100644 --- a/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFXS.java +++ b/src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFXS.java @@ -176,17 +176,18 @@ // driveConfig.MotorOutput.NeutralMode = NeutralModeValue.Brake; // driveConfig.Slot0 = constants.DriveMotorGains; // driveConfig.ExternalFeedback.SensorToMechanismRatio = constants.DriveMotorGearRatio; -// driveConfig.CurrentLimits.StatorCurrentLimit = DrivebaseConstants.kSlipCurrent; +// driveConfig.CurrentLimits.StatorCurrentLimit = DrivebaseConstants.kSlipCurrentAmps; // driveConfig.CurrentLimits.StatorCurrentLimitEnable = true; // // Build the OpenLoopRampsConfigs and ClosedLoopRampsConfigs for current smoothing // OpenLoopRampsConfigs openRamps = new OpenLoopRampsConfigs(); -// openRamps.DutyCycleOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; -// openRamps.VoltageOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; -// openRamps.TorqueOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriod; +// openRamps.DutyCycleOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; +// openRamps.VoltageOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; +// openRamps.TorqueOpenLoopRampPeriod = DrivebaseConstants.kDriveOpenLoopRampPeriodSecs; // ClosedLoopRampsConfigs closedRamps = new ClosedLoopRampsConfigs(); -// closedRamps.DutyCycleClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; -// closedRamps.VoltageClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; -// closedRamps.TorqueClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriod; +// closedRamps.DutyCycleClosedLoopRampPeriod = +// DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; +// closedRamps.VoltageClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; +// closedRamps.TorqueClosedLoopRampPeriod = DrivebaseConstants.kDriveClosedLoopRampPeriodSecs; // // Apply the open- and closed-loop ramp configuration for current smoothing // driveConfig.withClosedLoopRamps(closedRamps).withOpenLoopRamps(openRamps); // // Set motor inversions @@ -388,7 +389,7 @@ // public void setDriveOpenLoop(double output) { // // Scale by actual battery voltage to keep full output consistent // double busVoltage = RobotController.getBatteryVoltage(); -// double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; +// double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; // driveTalon.setControl( // switch (m_DriveMotorClosedLoopOutput) { @@ -409,7 +410,7 @@ // @Override // public void setTurnOpenLoop(double output) { // double busVoltage = RobotController.getBatteryVoltage(); -// double scaledOutput = output * DrivebaseConstants.kOptimalVoltage / busVoltage; +// double scaledOutput = output * DrivebaseConstants.kNominalVoltage / busVoltage; // turnTalon.setControl( // switch (m_SteerMotorClosedLoopOutput) { @@ -446,7 +447,7 @@ // Math.signum(velocityRotPerSec) * DrivebaseConstants.kDriveS // + DrivebaseConstants.kDriveV * velocityRotPerSec // + DrivebaseConstants.kDriveA * accelerationRotPerSec2; -// double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; +// double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; // // Set the drive motor control based on CTRE LICENSED status // driveTalon.setControl( @@ -478,8 +479,8 @@ // double busVoltage = RobotController.getBatteryVoltage(); // // Scale feedforward voltage by battery voltage -// double nominalFFVolts = DrivebaseConstants.kNominalFFVolts; -// double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kOptimalVoltage / busVoltage; +// double nominalFFVolts = DrivebaseConstants.kNominalFeedforwardVolts; +// double scaledFFVolts = nominalFFVolts * DrivebaseConstants.kNominalVoltage / busVoltage; // turnTalon.setControl( // switch (m_SteerMotorClosedLoopOutput) { diff --git a/src/main/java/frc/robot/subsystems/drive/PhoenixOdometryThread.java b/src/main/java/frc/robot/subsystems/drive/PhoenixOdometryThread.java index 75eac9d0..e19f5eec 100644 --- a/src/main/java/frc/robot/subsystems/drive/PhoenixOdometryThread.java +++ b/src/main/java/frc/robot/subsystems/drive/PhoenixOdometryThread.java @@ -12,8 +12,9 @@ import com.ctre.phoenix6.BaseStatusSignal; import com.ctre.phoenix6.StatusSignal; import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.RobotController; -import frc.robot.generated.TunerConstants; +import frc.robot.generated.TunerFactory; import java.util.ArrayList; import java.util.List; import java.util.Queue; @@ -40,7 +41,7 @@ public class PhoenixOdometryThread extends Thread { private final List> genericQueues = new ArrayList<>(); private final List> timestampQueues = new ArrayList<>(); - private static boolean isCANFD = TunerConstants.kCANBus.isNetworkFD(); + private static final boolean isCANFD = TunerFactory.INSTANCE.canBus().isNetworkFD(); private static PhoenixOdometryThread instance = null; private long droppedSamples = 0; @@ -126,7 +127,9 @@ public void run() { if (phoenixSignals.length > 0) BaseStatusSignal.refreshAll(phoenixSignals); } } catch (InterruptedException e) { - e.printStackTrace(); + DriverStation.reportWarning("Phoenix odometry thread interrupted", e.getStackTrace()); + Thread.currentThread().interrupt(); + return; } finally { signalsLock.unlock(); } @@ -151,7 +154,7 @@ public void run() { if (!phoenixQueues.get(i).offer(phoenixSignals[i].getValueAsDouble())) droppedSamples++; } for (int i = 0; i < genericSignals.size(); i++) { - genericQueues.get(i).offer(genericSignals.get(i).getAsDouble()); + if (!genericQueues.get(i).offer(genericSignals.get(i).getAsDouble())) droppedSamples++; } for (int i = 0; i < timestampQueues.size(); i++) { if (!timestampQueues.get(i).offer(timestamp)) droppedSamples++; diff --git a/src/main/java/frc/robot/subsystems/drive/SparkOdometryThread.java b/src/main/java/frc/robot/subsystems/drive/SparkOdometryThread.java index 0a5d052a..f4214b10 100644 --- a/src/main/java/frc/robot/subsystems/drive/SparkOdometryThread.java +++ b/src/main/java/frc/robot/subsystems/drive/SparkOdometryThread.java @@ -18,6 +18,7 @@ import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.function.DoubleSupplier; +import org.littletonrobotics.junction.Logger; /** * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. @@ -35,6 +36,8 @@ public class SparkOdometryThread { private static SparkOdometryThread instance = null; private Notifier notifier = new Notifier(this::run); + private long droppedSamples = 0; + private long loopCount = 0; public static SparkOdometryThread getInstance() { if (instance == null) { @@ -112,17 +115,21 @@ private void run() { // If valid, add values to queues if (isValid) { for (int i = 0; i < sparkSignals.size(); i++) { - sparkQueues.get(i).offer(sparkValues[i]); + if (!sparkQueues.get(i).offer(sparkValues[i])) droppedSamples++; } for (int i = 0; i < genericSignals.size(); i++) { - genericQueues.get(i).offer(genericSignals.get(i).getAsDouble()); + if (!genericQueues.get(i).offer(genericSignals.get(i).getAsDouble())) droppedSamples++; } for (int i = 0; i < timestampQueues.size(); i++) { - timestampQueues.get(i).offer(timestamp); + if (!timestampQueues.get(i).offer(timestamp)) droppedSamples++; } } } finally { Drive.odometryLock.unlock(); } + + if ((loopCount++ % (int) SwerveConstants.kOdometryFrequency) == 0) { + Logger.recordOutput("Drive/SparkOdomThread/DroppedSamples", droppedSamples); + } } } diff --git a/src/main/java/frc/robot/subsystems/drive/SwerveConstants.java b/src/main/java/frc/robot/subsystems/drive/SwerveConstants.java index ceaff2ba..50da778b 100644 --- a/src/main/java/frc/robot/subsystems/drive/SwerveConstants.java +++ b/src/main/java/frc/robot/subsystems/drive/SwerveConstants.java @@ -17,7 +17,7 @@ import edu.wpi.first.math.util.Units; import frc.robot.Constants; import frc.robot.Constants.CANBuses; -import frc.robot.generated.TunerConstants; +import frc.robot.generated.TunerFactory; import frc.robot.subsystems.imu.ImuIO; import frc.robot.subsystems.imu.ImuIONavX; import frc.robot.subsystems.imu.ImuIOPigeon2; @@ -108,86 +108,86 @@ public class SwerveConstants { // Fill in the values from the proper source static { + var tuner = TunerFactory.INSTANCE; + switch (Constants.getSwerveType()) { case PHOENIX6: kImuType = "pigeon2"; - kCoupleRatio = TunerConstants.FrontLeft.CouplingGearRatio; - kDriveGearRatio = TunerConstants.FrontLeft.DriveMotorGearRatio; - kSteerGearRatio = TunerConstants.FrontLeft.SteerMotorGearRatio; + kCoupleRatio = tuner.frontLeft().CouplingGearRatio; + kDriveGearRatio = tuner.frontLeft().DriveMotorGearRatio; + kSteerGearRatio = tuner.frontLeft().SteerMotorGearRatio; kCANbusName = CANBuses.DRIVE; - kPigeonId = TunerConstants.DrivetrainConstants.Pigeon2Id; - kSteerInertia = TunerConstants.FrontLeft.SteerInertia; - kDriveInertia = TunerConstants.FrontLeft.DriveInertia; + kPigeonId = tuner.drivetrain().Pigeon2Id; + kSteerInertia = tuner.frontLeft().SteerInertia; + kDriveInertia = tuner.frontLeft().DriveInertia; kSteerFrictionVoltage = 0.0; kDriveFrictionVoltage = 0.1; kSteerCurrentLimit = 40.0; // Example from CTRE documentation kDriveCurrentLimit = 120.0; // Example from CTRE documentation // Front Left - kFLDriveMotorId = TunerConstants.FrontLeft.DriveMotorId; - kFLSteerMotorId = TunerConstants.FrontLeft.SteerMotorId; - kFLEncoderId = TunerConstants.FrontLeft.EncoderId; + kFLDriveMotorId = tuner.frontLeft().DriveMotorId; + kFLSteerMotorId = tuner.frontLeft().SteerMotorId; + kFLEncoderId = tuner.frontLeft().EncoderId; kFLDriveCanbus = kCANbusName; kFLSteerCanbus = kCANbusName; kFLEncoderCanbus = kCANbusName; kFLDriveType = "kraken"; kFLSteerType = "kraken"; kFLEncoderType = "cancoder"; - kFLEncoderOffset = - -Units.rotationsToRadians(TunerConstants.FrontLeft.EncoderOffset) + Math.PI; - kFLDriveInvert = TunerConstants.FrontLeft.DriveMotorInverted; - kFLSteerInvert = TunerConstants.FrontLeft.SteerMotorInverted; - kFLEncoderInvert = TunerConstants.FrontLeft.EncoderInverted; - kFLXPosMeters = TunerConstants.FrontLeft.LocationX; - kFLYPosMeters = TunerConstants.FrontLeft.LocationY; + kFLEncoderOffset = -Units.rotationsToRadians(tuner.frontLeft().EncoderOffset) + Math.PI; + kFLDriveInvert = tuner.frontLeft().DriveMotorInverted; + kFLSteerInvert = tuner.frontLeft().SteerMotorInverted; + kFLEncoderInvert = tuner.frontLeft().EncoderInverted; + kFLXPosMeters = tuner.frontLeft().LocationX; + kFLYPosMeters = tuner.frontLeft().LocationY; // Front Right - kFRDriveMotorId = TunerConstants.FrontRight.DriveMotorId; - kFRSteerMotorId = TunerConstants.FrontRight.SteerMotorId; - kFREncoderId = TunerConstants.FrontRight.EncoderId; + kFRDriveMotorId = tuner.frontRight().DriveMotorId; + kFRSteerMotorId = tuner.frontRight().SteerMotorId; + kFREncoderId = tuner.frontRight().EncoderId; kFRDriveCanbus = kCANbusName; kFRSteerCanbus = kCANbusName; kFREncoderCanbus = kCANbusName; kFRDriveType = "kraken"; kFRSteerType = "kraken"; kFREncoderType = "cancoder"; - kFREncoderOffset = -Units.rotationsToRadians(TunerConstants.FrontRight.EncoderOffset); - kFRDriveInvert = TunerConstants.FrontRight.DriveMotorInverted; - kFRSteerInvert = TunerConstants.FrontRight.SteerMotorInverted; - kFREncoderInvert = TunerConstants.FrontRight.EncoderInverted; - kFRXPosMeters = TunerConstants.FrontRight.LocationX; - kFRYPosMeters = TunerConstants.FrontRight.LocationY; + kFREncoderOffset = -Units.rotationsToRadians(tuner.frontRight().EncoderOffset); + kFRDriveInvert = tuner.frontRight().DriveMotorInverted; + kFRSteerInvert = tuner.frontRight().SteerMotorInverted; + kFREncoderInvert = tuner.frontRight().EncoderInverted; + kFRXPosMeters = tuner.frontRight().LocationX; + kFRYPosMeters = tuner.frontRight().LocationY; // Back Left - kBLDriveMotorId = TunerConstants.BackLeft.DriveMotorId; - kBLSteerMotorId = TunerConstants.BackLeft.SteerMotorId; - kBLEncoderId = TunerConstants.BackLeft.EncoderId; + kBLDriveMotorId = tuner.backLeft().DriveMotorId; + kBLSteerMotorId = tuner.backLeft().SteerMotorId; + kBLEncoderId = tuner.backLeft().EncoderId; kBLDriveCanbus = kCANbusName; kBLSteerCanbus = kCANbusName; kBLEncoderCanbus = kCANbusName; kBLDriveType = "kraken"; kBLSteerType = "kraken"; kBLEncoderType = "cancoder"; - kBLEncoderOffset = - -Units.rotationsToRadians(TunerConstants.BackLeft.EncoderOffset) + Math.PI; - kBLDriveInvert = TunerConstants.BackLeft.DriveMotorInverted; - kBLSteerInvert = TunerConstants.BackLeft.SteerMotorInverted; - kBLEncoderInvert = TunerConstants.BackLeft.EncoderInverted; - kBLXPosMeters = TunerConstants.BackLeft.LocationX; - kBLYPosMeters = TunerConstants.BackLeft.LocationY; + kBLEncoderOffset = -Units.rotationsToRadians(tuner.backLeft().EncoderOffset) + Math.PI; + kBLDriveInvert = tuner.backLeft().DriveMotorInverted; + kBLSteerInvert = tuner.backLeft().SteerMotorInverted; + kBLEncoderInvert = tuner.backLeft().EncoderInverted; + kBLXPosMeters = tuner.backLeft().LocationX; + kBLYPosMeters = tuner.backLeft().LocationY; // Back Right - kBRDriveMotorId = TunerConstants.BackRight.DriveMotorId; - kBRSteerMotorId = TunerConstants.BackRight.SteerMotorId; - kBREncoderId = TunerConstants.BackRight.EncoderId; + kBRDriveMotorId = tuner.backRight().DriveMotorId; + kBRSteerMotorId = tuner.backRight().SteerMotorId; + kBREncoderId = tuner.backRight().EncoderId; kBRDriveCanbus = kCANbusName; kBRSteerCanbus = kCANbusName; kBREncoderCanbus = kCANbusName; kBRDriveType = "kraken"; kBRSteerType = "kraken"; kBREncoderType = "cancoder"; - kBREncoderOffset = -Units.rotationsToRadians(TunerConstants.BackRight.EncoderOffset); - kBRDriveInvert = TunerConstants.BackRight.DriveMotorInverted; - kBRSteerInvert = TunerConstants.BackRight.SteerMotorInverted; - kBREncoderInvert = TunerConstants.BackRight.EncoderInverted; - kBRXPosMeters = TunerConstants.BackRight.LocationX; - kBRYPosMeters = TunerConstants.BackRight.LocationY; + kBREncoderOffset = -Units.rotationsToRadians(tuner.backRight().EncoderOffset); + kBRDriveInvert = tuner.backRight().DriveMotorInverted; + kBRSteerInvert = tuner.backRight().SteerMotorInverted; + kBREncoderInvert = tuner.backRight().EncoderInverted; + kBRXPosMeters = tuner.backRight().LocationX; + kBRYPosMeters = tuner.backRight().LocationY; break; case YAGSL: diff --git a/src/main/java/frc/robot/subsystems/flywheel_example/Flywheel.java b/src/main/java/frc/robot/subsystems/flywheel_example/Flywheel.java index 80406e50..35d446b2 100644 --- a/src/main/java/frc/robot/subsystems/flywheel_example/Flywheel.java +++ b/src/main/java/frc/robot/subsystems/flywheel_example/Flywheel.java @@ -9,10 +9,14 @@ package frc.robot.subsystems.flywheel_example; +import static edu.wpi.first.units.Units.Second; +import static edu.wpi.first.units.Units.Seconds; import static edu.wpi.first.units.Units.Volts; import static frc.robot.Constants.FlywheelConstants.*; +import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.RobotController; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.Constants; @@ -23,7 +27,8 @@ public class Flywheel extends RBSISubsystem { private final FlywheelIO io; private final FlywheelIOInputsAutoLogged inputs = new FlywheelIOInputsAutoLogged(); - private final SysIdRoutine sysId; + private final SysIdRoutine voltageSysId; + private final SysIdRoutine dutyCycleSysId; /** Creates a new Flywheel. */ public Flywheel(FlywheelIO io) { @@ -34,23 +39,17 @@ public Flywheel(FlywheelIO io) { switch (Constants.getMode()) { case REAL: case REPLAY: - io.configureGains(kPreal, 0.0, kDreal, kSreal, kVreal, kAreal); + io.configureGains(kRealP, 0.0, kRealD, kRealS, kRealV, kRealA); break; case SIM: default: - io.configureGains(kPsim, 0.0, kDsim, kSsim, kVsim, kAsim); + io.configureGains(kSimP, 0.0, kSimD, kSimS, kSimV, kSimA); break; } - // Configure SysId - sysId = - new SysIdRoutine( - new SysIdRoutine.Config( - null, - null, - null, - (state) -> Logger.recordOutput("Flywheel/SysIdState", state.toString())), - new SysIdRoutine.Mechanism((voltage) -> runVolts(voltage.in(Volts)), null, this)); + // Configure SysId routines. The voltage routine is the preferred source for kS/kV/kA fits. + voltageSysId = createSysIdRoutine("FlywheelVoltage", this::runVolts); + dutyCycleSysId = createSysIdRoutine("FlywheelDutyCycle", this::runDutyCycleForSysIdVolts); } /** Periodic function -- inherits timing logic from RBSISubsystem */ @@ -65,6 +64,17 @@ public void runVolts(double volts) { io.setVoltage(volts); } + /** + * Run open loop using duty cycle while treating the SysId output as requested motor voltage. + * + *

This is useful for comparing vendor duty-cycle behavior against direct voltage control. Use + * the logged applied voltage, not the requested duty cycle, when fitting SysId data. + */ + public void runDutyCycleForSysIdVolts(double volts) { + double batteryVoltage = RobotController.getBatteryVoltage(); + io.setPercent(batteryVoltage > 0.0 ? MathUtil.clamp(volts / batteryVoltage, -1.0, 1.0) : 0.0); + } + /** Run closed loop at the specified velocity. */ public void runVelocity(double velocityRPM) { var velocityRadPerSec = Units.rotationsPerMinuteToRadiansPerSecond(velocityRPM); @@ -74,6 +84,16 @@ public void runVelocity(double velocityRPM) { Logger.recordOutput("Flywheel/SetpointRPM", velocityRPM); } + /** + * Run profiled/smoothed closed loop at the specified velocity, when supported by the IO layer. + */ + public void runVelocityProfiled(double velocityRPM) { + var velocityRadPerSec = Units.rotationsPerMinuteToRadiansPerSecond(velocityRPM); + io.setVelocityProfiled(velocityRadPerSec); + + Logger.recordOutput("Flywheel/ProfiledSetpointRPM", velocityRPM); + } + /** Stops the flywheel. */ public void stop() { io.stop(); @@ -81,12 +101,40 @@ public void stop() { /** Returns a command to run a quasistatic test in the specified direction. */ public Command sysIdQuasistatic(SysIdRoutine.Direction direction) { - return sysId.quasistatic(direction); + return sysIdVoltageQuasistatic(direction); } /** Returns a command to run a dynamic test in the specified direction. */ public Command sysIdDynamic(SysIdRoutine.Direction direction) { - return sysId.dynamic(direction); + return sysIdVoltageDynamic(direction); + } + + /** Returns a command to run a direct-voltage quasistatic test. */ + public Command sysIdVoltageQuasistatic(SysIdRoutine.Direction direction) { + return voltageSysId + .quasistatic(direction) + .withName("Flywheel SysId Voltage " + directionLabel(direction) + " Quasistatic"); + } + + /** Returns a command to run a direct-voltage dynamic test. */ + public Command sysIdVoltageDynamic(SysIdRoutine.Direction direction) { + return voltageSysId + .dynamic(direction) + .withName("Flywheel SysId Voltage " + directionLabel(direction) + " Dynamic"); + } + + /** Returns a command to run a duty-cycle quasistatic test. */ + public Command sysIdDutyCycleQuasistatic(SysIdRoutine.Direction direction) { + return dutyCycleSysId + .quasistatic(direction) + .withName("Flywheel SysId Duty Cycle " + directionLabel(direction) + " Quasistatic"); + } + + /** Returns a command to run a duty-cycle dynamic test. */ + public Command sysIdDutyCycleDynamic(SysIdRoutine.Direction direction) { + return dutyCycleSysId + .dynamic(direction) + .withName("Flywheel SysId Duty Cycle " + directionLabel(direction) + " Dynamic"); } /** Returns the current velocity in RPM. */ @@ -104,4 +152,25 @@ public double getCharacterizationVelocity() { public int[] getPowerPorts() { return io.getPowerPorts(); } + + private SysIdRoutine createSysIdRoutine(String name, java.util.function.DoubleConsumer output) { + return new SysIdRoutine( + new SysIdRoutine.Config( + Volts.of(kSysIdQuasistaticRampRateVoltsPerSec).per(Second), + Volts.of(kSysIdDynamicStepVoltageVolts), + Seconds.of(kSysIdTimeoutSecs), + (state) -> recordSysIdState(name, state)), + new SysIdRoutine.Mechanism( + (voltage) -> output.accept(voltage.in(Volts)), null, this, name)); + } + + private void recordSysIdState(String routineName, SysIdRoutine.State state) { + Logger.recordOutput("SysIdTestState", state.toString()); + Logger.recordOutput("Flywheel/SysIdRoutine", routineName); + Logger.recordOutput("Flywheel/SysIdState", state.toString()); + } + + private static String directionLabel(SysIdRoutine.Direction direction) { + return direction == SysIdRoutine.Direction.kForward ? "Forward" : "Reverse"; + } } diff --git a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIO.java b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIO.java index 47fa8748..ac4f7cf5 100644 --- a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIO.java +++ b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIO.java @@ -28,6 +28,11 @@ public default void updateInputs(FlywheelIOInputs inputs) {} /** Run closed loop at the specified velocity. */ public default void setVelocity(double velocityRadPerSec) {} + /** Run closed loop at the specified velocity using a profiled/smoothed velocity request. */ + public default void setVelocityProfiled(double velocityRadPerSec) { + setVelocity(velocityRadPerSec); + } + /** Set gain constants */ public default void configureGains(double kP, double kI, double kD, double kS, double kV) {} diff --git a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSim.java b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSim.java index a8e8c20f..34ce798d 100644 --- a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSim.java +++ b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSim.java @@ -16,21 +16,18 @@ import edu.wpi.first.math.system.LinearSystem; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.math.system.plant.LinearSystemId; -import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.RobotController; import edu.wpi.first.wpilibj.simulation.FlywheelSim; +import frc.robot.Constants; +import frc.robot.Constants.FlywheelConstants; public class FlywheelIOSim implements FlywheelIO { - // Reduction between motors and encoder, as output over input. If the flywheel - // spins slower than the motors, this number should be greater than one. - private static final double kFlywheelGearing = 1.0; - - // 1/2 MR^2 - private static final double kFlywheelMomentOfInertia = - 0.5 * Units.lbsToKilograms(1.5) * Math.pow(Units.inchesToMeters(4), 2); - private final DCMotor m_gearbox = DCMotor.getNEO(1); private final LinearSystem m_plant = - LinearSystemId.createFlywheelSystem(m_gearbox, kFlywheelGearing, kFlywheelMomentOfInertia); + LinearSystemId.createFlywheelSystem( + m_gearbox, + FlywheelConstants.kSimGearing, + FlywheelConstants.kSimMomentOfInertiaKgMetersSq); private final FlywheelSim sim = new FlywheelSim(m_plant, m_gearbox); private PIDController pid = new PIDController(0.0, 0.0, 0.0); @@ -44,11 +41,14 @@ public class FlywheelIOSim implements FlywheelIO { public void updateInputs(FlywheelIOInputs inputs) { if (closedLoop) { appliedVolts = - MathUtil.clamp(pid.calculate(sim.getAngularVelocityRadPerSec()) + ffVolts, -12.0, 12.0); + MathUtil.clamp( + pid.calculate(sim.getAngularVelocityRadPerSec()) + ffVolts, + -FlywheelConstants.kMaxVoltage, + FlywheelConstants.kMaxVoltage); sim.setInputVoltage(appliedVolts); } - sim.update(0.02); + sim.update(Constants.kLoopPeriodSecs); inputs.positionRad = 0.0; inputs.velocityRadPerSec = sim.getAngularVelocityRadPerSec(); @@ -63,6 +63,11 @@ public void setVoltage(double volts) { sim.setInputVoltage(volts); } + @Override + public void setPercent(double percent) { + setVoltage(percent * RobotController.getBatteryVoltage()); + } + @Override public void setVelocity(double velocityRadPerSec) { closedLoop = true; diff --git a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSpark.java b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSpark.java index ee89859e..e847f8cf 100644 --- a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSpark.java +++ b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOSpark.java @@ -29,7 +29,7 @@ import com.revrobotics.spark.SparkLowLevel.MotorType; import com.revrobotics.spark.SparkMax; import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; -import com.revrobotics.spark.config.SparkFlexConfig; +import com.revrobotics.spark.config.SparkMaxConfig; import edu.wpi.first.math.controller.SimpleMotorFeedforward; import edu.wpi.first.math.util.Units; import frc.robot.Constants; @@ -55,55 +55,70 @@ public class FlywheelIOSpark implements FlywheelIO { public final int[] powerPorts = { FLYWHEEL_LEADER.getPowerPort(), FLYWHEEL_FOLLOWER.getPowerPort() }; - private final SimpleMotorFeedforward ff = new SimpleMotorFeedforward(kSreal, kVreal, kAreal); + private final SparkMaxConfig leaderConfig = new SparkMaxConfig(); + private SimpleMotorFeedforward ff = new SimpleMotorFeedforward(kRealS, kRealV, kRealA); + + @Override + public int[] powerPorts() { + return powerPorts; + } public FlywheelIOSpark() { // Configure leader motor - var leaderConfig = new SparkFlexConfig(); leaderConfig .idleMode( - switch (kFlywheelIdleMode) { + switch (kIdleMode) { case COAST -> IdleMode.kCoast; case BRAKE -> IdleMode.kBrake; }) .smartCurrentLimit((int) SwerveConstants.kDriveCurrentLimit) - .voltageCompensation(DrivebaseConstants.kOptimalVoltage); + .voltageCompensation(DrivebaseConstants.kNominalVoltage); leaderConfig.encoder.uvwMeasurementPeriod(10).uvwAverageDepth(2); leaderConfig .closedLoop .feedbackSensor(FeedbackSensor.kPrimaryEncoder) - .pid(kPreal, 0.0, kDreal) + .pid(kRealP, 0.0, kRealD) .feedForward - .kS(kSreal) - .kV(kVreal) - .kA(kAreal); + .kS(kRealS) + .kV(kRealV) + .kA(kRealA); leaderConfig .signals .primaryEncoderPositionAlwaysOn(true) .primaryEncoderPositionPeriodMs((int) (1000.0 / SwerveConstants.kOdometryFrequency)) .primaryEncoderVelocityAlwaysOn(true) - .primaryEncoderVelocityPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .appliedOutputPeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .busVoltagePeriodMs((int) (Constants.loopPeriodSecs * 1000.)) - .outputCurrentPeriodMs((int) (Constants.loopPeriodSecs * 1000.)); + .primaryEncoderVelocityPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .appliedOutputPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .busVoltagePeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)) + .outputCurrentPeriodMs((int) (Constants.kLoopPeriodSecs * 1000.)); leaderConfig - .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriod) - .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriod); + .openLoopRampRate(DrivebaseConstants.kDriveOpenLoopRampPeriodSecs) + .closedLoopRampRate(DrivebaseConstants.kDriveClosedLoopRampPeriodSecs); SparkUtil.tryUntilOk( leader, 5, () -> leader.configure( leaderConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters)); + + var followerConfig = new SparkMaxConfig(); + followerConfig.follow(leader); + SparkUtil.tryUntilOk( + follower, + 5, + () -> + follower.configure( + followerConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters)); + SparkUtil.tryUntilOk(leader, 5, () -> encoder.setPosition(0.0)); } @Override public void updateInputs(FlywheelIOInputs inputs) { - inputs.positionRad = Units.rotationsToRadians(encoder.getPosition() / kFlywheelGearRatio); + inputs.positionRad = Units.rotationsToRadians(encoder.getPosition() / kGearRatio); inputs.velocityRadPerSec = - Units.rotationsPerMinuteToRadiansPerSecond(encoder.getVelocity() / kFlywheelGearRatio); + Units.rotationsPerMinuteToRadiansPerSecond(encoder.getVelocity() / kGearRatio); inputs.appliedVolts = leader.getAppliedOutput() * leader.getBusVoltage(); inputs.currentAmps = new double[] {leader.getOutputCurrent(), follower.getOutputCurrent()}; @@ -120,11 +135,16 @@ public void setVoltage(double volts) { leader.setVoltage(volts); } + @Override + public void setPercent(double percent) { + leader.set(percent); + } + @Override public void setVelocity(double velocityRadPerSec) { double ffVolts = ff.calculate(velocityRadPerSec); pid.setSetpoint( - Units.radiansPerSecondToRotationsPerMinute(velocityRadPerSec) * kFlywheelGearRatio, + Units.radiansPerSecondToRotationsPerMinute(velocityRadPerSec) * kGearRatio, ControlType.kVelocity, ClosedLoopSlot.kSlot0, ffVolts, @@ -139,23 +159,23 @@ public void stop() { /** * Configure the closed-loop control gains * - *

TODO: This functionality is no longer supported by the REVLib SparkClosedLoopController - * class. In order to keep control of the flywheel's underlying funtionality, shift everything to - * SmartMotion control. + *

REVLib 2026 applies closed-loop gains through the Spark configuration object rather than + * direct setters on {@link SparkClosedLoopController}. */ @Override public void configureGains(double kP, double kI, double kD, double kS, double kV) { - // pid.setP(kP, 0); - // pid.setI(kI, 0); - // pid.setD(kD, 0); - // pid.setFF(0, 0); + configureGains(kP, kI, kD, kS, kV, 0.0); } @Override public void configureGains(double kP, double kI, double kD, double kS, double kV, double kA) { - // pid.setP(kP, 0); - // pid.setI(kI, 0); - // pid.setD(kD, 0); - // pid.setFF(0, 0); + ff = new SimpleMotorFeedforward(kS, kV, kA); + leaderConfig.closedLoop.pid(kP, kI, kD).feedForward.kS(kS).kV(kV).kA(kA); + SparkUtil.tryUntilOk( + leader, + 5, + () -> + leader.configure( + leaderConfig, ResetMode.kNoResetSafeParameters, PersistMode.kNoPersistParameters)); } } diff --git a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOTalonFX.java b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOTalonFX.java index 1b964f5c..a9bb0f91 100644 --- a/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOTalonFX.java +++ b/src/main/java/frc/robot/subsystems/flywheel_example/FlywheelIOTalonFX.java @@ -17,10 +17,11 @@ import com.ctre.phoenix6.configs.ClosedLoopRampsConfigs; import com.ctre.phoenix6.configs.OpenLoopRampsConfigs; import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.controls.DutyCycleOut; import com.ctre.phoenix6.controls.Follower; -import com.ctre.phoenix6.controls.MotionMagicDutyCycle; import com.ctre.phoenix6.controls.MotionMagicVelocityVoltage; -import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.controls.VelocityVoltage; +import com.ctre.phoenix6.controls.VoltageOut; import com.ctre.phoenix6.hardware.TalonFX; import com.ctre.phoenix6.signals.MotorAlignmentValue; import com.ctre.phoenix6.signals.NeutralModeValue; @@ -46,6 +47,11 @@ public class FlywheelIOTalonFX implements FlywheelIO { FLYWHEEL_LEADER.getPowerPort(), FLYWHEEL_FOLLOWER.getPowerPort() }; + @Override + public int[] powerPorts() { + return powerPorts; + } + private final StatusSignal leaderPosition = leader.getPosition(); private final StatusSignal leaderVelocity = leader.getVelocity(); private final StatusSignal leaderAppliedVolts = leader.getMotorVoltage(); @@ -54,31 +60,36 @@ public class FlywheelIOTalonFX implements FlywheelIO { private final TalonFXConfiguration config = new TalonFXConfiguration(); private final boolean isCTREPro = Constants.getPhoenixPro() == CTREPro.LICENSED; + private final VoltageOut voltageRequest = new VoltageOut(0); + private final DutyCycleOut dutyCycleRequest = new DutyCycleOut(0); + private final VelocityVoltage velocityVoltageRequest = new VelocityVoltage(0); + private final MotionMagicVelocityVoltage motionMagicVelocityRequest = + new MotionMagicVelocityVoltage(0); public FlywheelIOTalonFX() { - config.CurrentLimits.SupplyCurrentLimit = PowerConstants.kMotorPortMaxCurrent; + config.CurrentLimits.SupplyCurrentLimit = PowerConstants.kMotorPortMaxCurrentAmps; config.CurrentLimits.SupplyCurrentLimitEnable = true; config.MotorOutput.NeutralMode = - switch (kFlywheelIdleMode) { + switch (kIdleMode) { case COAST -> NeutralModeValue.Coast; case BRAKE -> NeutralModeValue.Brake; }; // Build the OpenLoopRampsConfigs and ClosedLoopRampsConfigs for current smoothing OpenLoopRampsConfigs openRamps = new OpenLoopRampsConfigs(); - openRamps.DutyCycleOpenLoopRampPeriod = kFlywheelOpenLoopRampPeriod; - openRamps.VoltageOpenLoopRampPeriod = kFlywheelOpenLoopRampPeriod; - openRamps.TorqueOpenLoopRampPeriod = kFlywheelOpenLoopRampPeriod; + openRamps.DutyCycleOpenLoopRampPeriod = kOpenLoopRampPeriodSecs; + openRamps.VoltageOpenLoopRampPeriod = kOpenLoopRampPeriodSecs; + openRamps.TorqueOpenLoopRampPeriod = kOpenLoopRampPeriodSecs; ClosedLoopRampsConfigs closedRamps = new ClosedLoopRampsConfigs(); - closedRamps.DutyCycleClosedLoopRampPeriod = kFlywheelClosedLoopRampPeriod; - closedRamps.VoltageClosedLoopRampPeriod = kFlywheelClosedLoopRampPeriod; - closedRamps.TorqueClosedLoopRampPeriod = kFlywheelClosedLoopRampPeriod; + closedRamps.DutyCycleClosedLoopRampPeriod = kClosedLoopRampPeriodSecs; + closedRamps.VoltageClosedLoopRampPeriod = kClosedLoopRampPeriodSecs; + closedRamps.TorqueClosedLoopRampPeriod = kClosedLoopRampPeriodSecs; // Apply the open- and closed-loop ramp configuration for current smoothing config.withClosedLoopRamps(closedRamps).withOpenLoopRamps(openRamps); // set Motion Magic Velocity settings var motionMagicConfigs = config.MotionMagic; motionMagicConfigs.MotionMagicAcceleration = - 400; // Target acceleration of 400 rps/s (0.25 seconds to max) - motionMagicConfigs.MotionMagicJerk = 4000; // Target jerk of 4000 rps/s/s (0.1 seconds) + kMotionMagicAccelerationRotPerSecSq; // Target acceleration in rotations/s/s + motionMagicConfigs.MotionMagicJerk = kMotionMagicJerkRotPerSecCubed; // rotations/s/s/s // Apply the configurations to the flywheel motors PhoenixUtil.tryUntilOk(5, () -> leader.getConfigurator().apply(config, 0.25)); @@ -96,10 +107,9 @@ public FlywheelIOTalonFX() { public void updateInputs(FlywheelIOInputs inputs) { BaseStatusSignal.refreshAll( leaderPosition, leaderVelocity, leaderAppliedVolts, leaderCurrent, followerCurrent); - inputs.positionRad = - Units.rotationsToRadians(leaderPosition.getValueAsDouble()) / kFlywheelGearRatio; + inputs.positionRad = Units.rotationsToRadians(leaderPosition.getValueAsDouble()) / kGearRatio; inputs.velocityRadPerSec = - Units.rotationsToRadians(leaderVelocity.getValueAsDouble()) / kFlywheelGearRatio; + Units.rotationsToRadians(leaderVelocity.getValueAsDouble()) / kGearRatio; inputs.appliedVolts = leaderAppliedVolts.getValueAsDouble(); inputs.currentAmps = new double[] {leaderCurrent.getValueAsDouble(), followerCurrent.getValueAsDouble()}; @@ -107,25 +117,28 @@ public void updateInputs(FlywheelIOInputs inputs) { @Override public void setVoltage(double volts) { - final MotionMagicVoltage m_request = new MotionMagicVoltage(volts); - m_request.withEnableFOC(isCTREPro); - leader.setControl(m_request); + leader.setControl(voltageRequest.withOutput(volts).withEnableFOC(isCTREPro)); } @Override public void setVelocity(double velocityRadPerSec) { - // create a Motion Magic Velocity request, voltage output - final MotionMagicVelocityVoltage m_request = new MotionMagicVelocityVoltage(0); - m_request.withEnableFOC(isCTREPro); - leader.setControl(m_request.withVelocity(Units.radiansToRotations(velocityRadPerSec))); + leader.setControl( + velocityVoltageRequest + .withVelocity(Units.radiansToRotations(velocityRadPerSec) * kGearRatio) + .withEnableFOC(isCTREPro)); + } + + @Override + public void setVelocityProfiled(double velocityRadPerSec) { + leader.setControl( + motionMagicVelocityRequest + .withVelocity(Units.radiansToRotations(velocityRadPerSec) * kGearRatio) + .withEnableFOC(isCTREPro)); } @Override public void setPercent(double percent) { - // create a Motion Magic DutyCycle request, voltage output - final MotionMagicDutyCycle m_request = new MotionMagicDutyCycle(percent); - m_request.withEnableFOC(isCTREPro); - leader.setControl(m_request); + leader.setControl(dutyCycleRequest.withOutput(percent).withEnableFOC(isCTREPro)); } @Override diff --git a/src/main/java/frc/robot/subsystems/imu/Imu.java b/src/main/java/frc/robot/subsystems/imu/Imu.java index 854f7182..c163a2e0 100644 --- a/src/main/java/frc/robot/subsystems/imu/Imu.java +++ b/src/main/java/frc/robot/subsystems/imu/Imu.java @@ -54,6 +54,10 @@ public void rbsiPeriodic() { io.updateInputs(inputs); } + public boolean isConnected() { + return inputs.connected; + } + /** * Get the inputs objects * diff --git a/src/main/java/frc/robot/subsystems/imu/ImuIO.java b/src/main/java/frc/robot/subsystems/imu/ImuIO.java index 8ee521e4..1b2bea5f 100644 --- a/src/main/java/frc/robot/subsystems/imu/ImuIO.java +++ b/src/main/java/frc/robot/subsystems/imu/ImuIO.java @@ -26,6 +26,7 @@ * odometry samples. */ public interface ImuIO extends RBSIIO { + double[] EMPTY_DOUBLE_ARRAY = new double[0]; @AutoLog class ImuIOInputs { @@ -44,9 +45,9 @@ class ImuIOInputs { // Time spent in the IO update call (seconds) public double latencySeconds = 0.0; // Odometry samples (timestamps in seconds) - public double[] odometryYawTimestamps = new double[] {}; + public double[] odometryYawTimestamps = EMPTY_DOUBLE_ARRAY; // Odometry samples (yaw positions in radians) - public double[] odometryYawPositionsRad = new double[] {}; + public double[] odometryYawPositionsRad = EMPTY_DOUBLE_ARRAY; } /** Update the current IMU readings into `inputs` */ diff --git a/src/main/java/frc/robot/subsystems/imu/ImuIONavX.java b/src/main/java/frc/robot/subsystems/imu/ImuIONavX.java index 97d47100..08a44b1e 100644 --- a/src/main/java/frc/robot/subsystems/imu/ImuIONavX.java +++ b/src/main/java/frc/robot/subsystems/imu/ImuIONavX.java @@ -88,9 +88,9 @@ public void updateInputs(ImuIOInputs inputs) { // World linear accel (NavX returns "g" typically); convert to m/s/s inputs.linearAccel = new Translation3d( - navx.getWorldLinearAccelX() * Constants.G_TO_MPS2, - navx.getWorldLinearAccelY() * Constants.G_TO_MPS2, - navx.getWorldLinearAccelZ() * Constants.G_TO_MPS2); + navx.getWorldLinearAccelX() * Constants.kGravityMetersPerSecSq, + navx.getWorldLinearAccelY() * Constants.kGravityMetersPerSecSq, + navx.getWorldLinearAccelZ() * Constants.kGravityMetersPerSecSq); // Jerk computed as (delta accel) / dt if (prevTimestampNs != 0L) { @@ -116,8 +116,8 @@ public void updateInputs(ImuIOInputs inputs) { inputs.odometryYawPositionsRad = yawOut; } else { // ...otherwise return empty arrays - inputs.odometryYawTimestamps = new double[] {}; - inputs.odometryYawPositionsRad = new double[] {}; + inputs.odometryYawTimestamps = EMPTY_DOUBLE_ARRAY; + inputs.odometryYawPositionsRad = EMPTY_DOUBLE_ARRAY; } // Compute how long this took in seconds diff --git a/src/main/java/frc/robot/subsystems/imu/ImuIOPigeon2.java b/src/main/java/frc/robot/subsystems/imu/ImuIOPigeon2.java index a1f708b0..7b203716 100644 --- a/src/main/java/frc/robot/subsystems/imu/ImuIOPigeon2.java +++ b/src/main/java/frc/robot/subsystems/imu/ImuIOPigeon2.java @@ -97,9 +97,9 @@ public void updateInputs(ImuIOInputs inputs) { // Accel: Phoenix returns "g" for these signals; convert to m/s/s inputs.linearAccel = new Translation3d( - accelX.getValueAsDouble() * Constants.G_TO_MPS2, - accelY.getValueAsDouble() * Constants.G_TO_MPS2, - accelZ.getValueAsDouble() * Constants.G_TO_MPS2); + accelX.getValueAsDouble() * Constants.kGravityMetersPerSecSq, + accelY.getValueAsDouble() * Constants.kGravityMetersPerSecSq, + accelZ.getValueAsDouble() * Constants.kGravityMetersPerSecSq); // Jerk computed as (delta accel) / dt if (prevTimestampNs != 0L) { @@ -126,8 +126,8 @@ public void updateInputs(ImuIOInputs inputs) { inputs.odometryYawPositionsRad = yawOut; } else { // ...otherwise return empty arrays - inputs.odometryYawTimestamps = new double[] {}; - inputs.odometryYawPositionsRad = new double[] {}; + inputs.odometryYawTimestamps = EMPTY_DOUBLE_ARRAY; + inputs.odometryYawPositionsRad = EMPTY_DOUBLE_ARRAY; } // Compute how long this took in seconds diff --git a/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java b/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java index 1bf04a7d..87321a75 100644 --- a/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java +++ b/src/main/java/frc/robot/subsystems/imu/ImuIOSim.java @@ -93,6 +93,7 @@ public void updateInputs(ImuIOInputs inputs) { inputs.odometryYawTimestamps = tsOut; inputs.odometryYawPositionsRad = yawOut; + clearOdomSamples(); // SIM logging Logger.recordOutput("IMU/YawRad", yawRad); @@ -109,6 +110,7 @@ public void updateInputs(ImuIOInputs inputs) { public void zeroYawRad(double yawRad) { this.yawRad = yawRad; this.yawRateRadPerSec = 0.0; + clearOdomSamples(); } private void pushOdomSample(double timestampSec, double yawRad) { @@ -120,4 +122,9 @@ private void pushOdomSample(double timestampSec, double yawRad) { if (odomSize < ODOM_CAP) odomSize++; } + + private void clearOdomSamples() { + odomSize = 0; + odomHead = 0; + } } diff --git a/src/main/java/frc/robot/subsystems/vision/Vision.java b/src/main/java/frc/robot/subsystems/vision/Vision.java index fda7b4b5..4d2b4ba7 100644 --- a/src/main/java/frc/robot/subsystems/vision/Vision.java +++ b/src/main/java/frc/robot/subsystems/vision/Vision.java @@ -28,6 +28,7 @@ import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; +import frc.robot.Constants; import frc.robot.Constants.Cameras; import frc.robot.FieldConstants; import frc.robot.subsystems.drive.Drive; @@ -37,6 +38,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.Optional; import java.util.OptionalDouble; import java.util.Set; @@ -87,6 +89,8 @@ public interface PoseMeasurementConsumer { // Variance minimum for fusing poses to prevent divide-by-zero explosions private static final double kMinVariance = 1e-12; + private static final Pose3d[] kEmptyTagPoseArray = new Pose3d[0]; + private static final int[] kEmptyTagIdArray = new int[0]; // Last smoothed and fused poses -- used for debugging private Pose2d lastFusedPose = new Pose2d(); @@ -94,6 +98,8 @@ public interface PoseMeasurementConsumer { private double lastFusedTs = Double.NaN; private boolean lastFusedValid = false; private boolean lastSmoothedValid = false; + private final LinkedHashSet tagIdsSeenThisLoop = new LinkedHashSet<>(); + private final ArrayList perCamAccepted = new ArrayList<>(); /** Constructor */ public Vision(Drive drive, PoseMeasurementConsumer consumer, VisionIO... io) { @@ -131,14 +137,16 @@ protected int getPeriodPriority() { @Override public void rbsiPeriodic() { - // Debugging values boolean hasAcceptedThisLoop = false; boolean hasFusedThisLoop = false; boolean hasSmoothedThisLoop = false; + final boolean tuningMode = Constants.isTuningMode(); try { lastAlignDbg.reset(); + tagIdsSeenThisLoop.clear(); + perCamAccepted.clear(); // Pose reset gate (clears smoothing state, resets per-cam monotonic gates) long epoch = drive.getPoseResetEpoch(); if (epoch != lastSeenPoseResetEpoch) { @@ -156,15 +164,14 @@ public void rbsiPeriodic() { } // Always-on “health” debug -- may consider removing this - Logger.recordOutput("Vision/Debug/ioLength", io.length); int totalObs = 0; for (int i = 0; i < io.length; i++) { totalObs += (inputs[i].poseObservations != null) ? inputs[i].poseObservations.length : 0; } - Logger.recordOutput("Vision/Debug/totalObsThisLoop", totalObs); - - // Choose best observation per camera for THIS loop - final ArrayList perCamAccepted = new ArrayList<>(io.length); + if (tuningMode) { + Logger.recordOutput("Vision/Debug/ioLength", io.length); + Logger.recordOutput("Vision/Debug/totalObsThisLoop", totalObs); + } for (int cam = 0; cam < io.length; cam++) { @@ -191,8 +198,17 @@ public void rbsiPeriodic() { for (var obs : obsArr) { seen++; + int[] tagIds = obs.usedTagIds(); + if (tagIds != null) { + for (int tagId : tagIds) { + tagIdsSeenThisLoop.add(tagId); + } + } + GateResult gate = passesScrutiny(cam, obs); - Logger.recordOutput("Vision/Camera" + cam + "/GateFail", gate.reason); + if (tuningMode) { + Logger.recordOutput("Vision/Camera" + cam + "/GateFail", gate.reason); + } if (!gate.accepted) { rejected++; continue; @@ -236,7 +252,9 @@ public void rbsiPeriodic() { Logger.recordOutput("Vision/Camera" + cam + "/ObsRejected", rejected); } - Logger.recordOutput("Vision/Debug/perCamAcceptedSize", perCamAccepted.size()); + if (tuningMode) { + Logger.recordOutput("Vision/Debug/perCamAcceptedSize", perCamAccepted.size()); + } if (perCamAccepted.isEmpty()) { // No new vision accepted this loop; we still log cached outputs below (in finally). @@ -246,8 +264,13 @@ public void rbsiPeriodic() { // ===== // Fuse all accepted cams at the newest timestamp among them - final double tFusion = - perCamAccepted.stream().mapToDouble(e -> e.timestampSeconds()).max().orElse(Double.NaN); + double tFusion = Double.NaN; + for (int i = 0; i < perCamAccepted.size(); i++) { + final double timestamp = perCamAccepted.get(i).timestampSeconds(); + if (!Double.isFinite(tFusion) || timestamp > tFusion) { + tFusion = timestamp; + } + } if (!Double.isFinite(tFusion)) return; final TimedPose fused = fuseAtTime(perCamAccepted, tFusion); @@ -285,6 +308,28 @@ public void rbsiPeriodic() { Logger.recordOutput("Vision/HasAcceptedThisLoop", hasAcceptedThisLoop); Logger.recordOutput("Vision/HasFusedThisLoop", hasFusedThisLoop); Logger.recordOutput("Vision/HasSmoothedThisLoop", hasSmoothedThisLoop); + + Logger.recordOutput("Vision/TagCountThisLoop", tagIdsSeenThisLoop.size()); + if (tagIdsSeenThisLoop.isEmpty()) { + Logger.recordOutput("Vision/TagsSeenThisLoop", kEmptyTagPoseArray); + Logger.recordOutput("Vision/TagIdsSeenThisLoop", kEmptyTagIdArray); + } else { + final Pose3d[] tagsSeen = new Pose3d[tagIdsSeenThisLoop.size()]; + final int[] tagIdsSeen = new int[tagIdsSeenThisLoop.size()]; + int tagCount = 0; + for (int tagId : tagIdsSeenThisLoop) { + tagIdsSeen[tagCount] = tagId; + Optional tagPose = FieldConstants.aprilTagLayout.getTagPose(tagId); + if (tagPose.isPresent()) { + tagsSeen[tagCount] = tagPose.get(); + tagCount++; + } + } + Logger.recordOutput( + "Vision/TagsSeenThisLoop", + tagCount == tagsSeen.length ? tagsSeen : Arrays.copyOf(tagsSeen, tagCount)); + Logger.recordOutput("Vision/TagIdsSeenThisLoop", tagIdsSeen); + } } } @@ -377,11 +422,11 @@ private GateResult passesScrutiny(int cam, VisionIO.PoseObservation obs) { if (obs.tagCount() <= 0) return new GateResult(false, "no tags"); // Single-tag ambiguity gate - if (obs.tagCount() == 1 && obs.ambiguity() > maxAmbiguity) + if (obs.tagCount() == 1 && obs.ambiguity() > kMaxAmbiguity) return new GateResult(false, "highly ambiguous"); // Z sanity - if (Math.abs(obs.pose().getZ()) > maxZError) return new GateResult(false, "z not sane"); + if (Math.abs(obs.pose().getZ()) > kMaxZErrorMeters) return new GateResult(false, "z not sane"); // Field bounds Pose3d p = obs.pose(); @@ -429,13 +474,13 @@ private BuiltEstimate buildEstimate(int cam, VisionIO.PoseObservation obs) { // Camera uncertainty factor final double camFactor = (cam < camConfigs.length) ? camConfigs[cam].stdDevFactor() : 1.0; - double linearStdDev = linearStdDevBaseline * camFactor * distFactor; - double angularStdDev = angularStdDevBaseline * camFactor * distFactor; + double linearStdDev = kLinearStdDevBaseline * camFactor * distFactor; + double angularStdDev = kAngularStdDevBaseline * camFactor * distFactor; // MegaTag2 bonus if applicable if (obs.type() == PoseObservationType.MEGATAG_2) { - linearStdDev *= linearStdDevMegatag2Factor; - angularStdDev *= angularStdDevMegatag2Factor; + linearStdDev *= kLinearStdDevMegatag2Factor; + angularStdDev *= kAngularStdDevMegatag2Factor; } // Trusted tag blending @@ -461,16 +506,17 @@ private BuiltEstimate buildEstimate(int cam, VisionIO.PoseObservation obs) { linearStdDev *= trustScale; angularStdDev *= trustScale; - linearStdDev = Math.max(linearStdDev, linearStdDevBaseline); - angularStdDev = Math.max(angularStdDev, angularStdDevBaseline); + linearStdDev = Math.max(linearStdDev, kLinearStdDevBaseline); + angularStdDev = Math.max(angularStdDev, kAngularStdDevBaseline); // Output logs for tuning - Logger.recordOutput("Vision/Camera" + cam + "/InjectedFracTrusted", fracTrusted); - - Logger.recordOutput("Vision/Camera" + cam + "/Dbg_linearStdDev", linearStdDev); - Logger.recordOutput("Vision/Camera" + cam + "/Dbg_angularStdDev", angularStdDev); - Logger.recordOutput("Vision/Camera" + cam + "/Dbg_avgDist", avgDist); - Logger.recordOutput("Vision/Camera" + cam + "/Dbg_tagCount", obs.tagCount()); + if (Constants.isTuningMode()) { + Logger.recordOutput("Vision/Camera" + cam + "/InjectedFracTrusted", fracTrusted); + Logger.recordOutput("Vision/Camera" + cam + "/Dbg_linearStdDev", linearStdDev); + Logger.recordOutput("Vision/Camera" + cam + "/Dbg_angularStdDev", angularStdDev); + Logger.recordOutput("Vision/Camera" + cam + "/Dbg_avgDist", avgDist); + Logger.recordOutput("Vision/Camera" + cam + "/Dbg_tagCount", obs.tagCount()); + } return new BuiltEstimate( new TimedPose( @@ -639,24 +685,30 @@ private TimedPose inverseVarianceFuse(ArrayList alignedAtTF, double t sumSin += rot.getSin() * wth; } - // If everything got skipped; return null - if (sumWx <= 0.0 || sumWy <= 0.0 || sumWth <= 0.0) return null; + // If every translational measurement got skipped, there is nothing useful to inject. + if (sumWx <= 0.0 || sumWy <= 0.0) return null; // Construct the fused translation final Translation2d fusedTranslation = new Translation2d(sumX / sumWx, sumY / sumWy); - // Rotation2d(cos, sin) will normalize internally; if both are ~0, fall back to zero. + // Rotation2d(cos, sin) will normalize internally. If all accepted observations have infinite + // angular uncertainty (e.g. MegaTag2), retain the newest aligned rotation and report infinite + // theta uncertainty so the downstream estimator ignores heading correction. + final boolean hasAngularWeight = sumWth > 0.0; final Rotation2d fusedRotation = - (Math.abs(sumCos) < 1e-12 && Math.abs(sumSin) < 1e-12) - ? Rotation2d.kZero - : new Rotation2d(sumCos, sumSin); + hasAngularWeight + ? new Rotation2d(sumCos, sumSin) + : alignedAtTF.get(alignedAtTF.size() - 1).pose().getRotation(); // The fused pose is the combination of translation and rotation final Pose2d fusedPose = new Pose2d(fusedTranslation, fusedRotation); // Fused standard deviations final Matrix fusedStdDevs = - VecBuilder.fill(Math.sqrt(1.0 / sumWx), Math.sqrt(1.0 / sumWy), Math.sqrt(1.0 / sumWth)); + VecBuilder.fill( + Math.sqrt(1.0 / sumWx), + Math.sqrt(1.0 / sumWy), + hasAngularWeight ? Math.sqrt(1.0 / sumWth) : Double.POSITIVE_INFINITY); // Construct and return the TimedPose objects return new TimedPose(fusedPose, tFusion, fusedStdDevs); @@ -692,8 +744,9 @@ private TimedPose smoothAtTime(double tFusion) { Pose2d alignedPose = timeAlignPose(e.pose(), e.timestampSeconds(), tFusion); if (alignedPose == null) continue; aligned.add(new TimedPose(alignedPose, tFusion, e.stdDevs())); - // Debugging Logging - Logger.recordOutput("Vision/Debug/deltaTime", tFusion - e.timestampSeconds()); + if (Constants.isTuningMode()) { + Logger.recordOutput("Vision/Debug/deltaTime", tFusion - e.timestampSeconds()); + } } if (aligned.isEmpty()) return fusedBuffer.peekLast(); diff --git a/src/main/java/frc/robot/subsystems/vision/VisionIOLimelight.java b/src/main/java/frc/robot/subsystems/vision/VisionIOLimelight.java index c9bbc0d3..dbbbe6f9 100644 --- a/src/main/java/frc/robot/subsystems/vision/VisionIOLimelight.java +++ b/src/main/java/frc/robot/subsystems/vision/VisionIOLimelight.java @@ -17,20 +17,31 @@ import edu.wpi.first.networktables.DoubleArraySubscriber; import edu.wpi.first.networktables.DoubleSubscriber; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.TimestampedDoubleArray; import edu.wpi.first.wpilibj.RobotController; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.function.Supplier; /** IO implementation for real Limelight hardware. */ public class VisionIOLimelight implements VisionIO { + private static final int BOTPOSE_MIN_LENGTH = 11; + private static final int BOTPOSE_LATENCY_INDEX = 6; + private static final int BOTPOSE_TAG_COUNT_INDEX = 7; + private static final int BOTPOSE_AVG_DISTANCE_INDEX = 9; + private static final int BOTPOSE_FIRST_TAG_ID_INDEX = 11; + private static final int BOTPOSE_FIRST_TAG_AMBIGUITY_INDEX = 17; + private static final int BOTPOSE_TAG_STRIDE = 7; + private static final long ORIENTATION_FLUSH_PERIOD_US = 20_000; + private static long lastOrientationFlushUs = 0; + private final Supplier rotationSupplier; private final DoubleArrayPublisher orientationPublisher; - private final DoubleSubscriber latencySubscriber; + private final DoubleSubscriber heartbeatSubscriber; private final DoubleSubscriber txSubscriber; private final DoubleSubscriber tySubscriber; private final DoubleArraySubscriber megatag1Subscriber; @@ -46,7 +57,7 @@ public VisionIOLimelight(String name, Supplier rotationSupplier) { var table = NetworkTableInstance.getDefault().getTable(name); this.rotationSupplier = rotationSupplier; orientationPublisher = table.getDoubleArrayTopic("robot_orientation_set").publish(); - latencySubscriber = table.getDoubleTopic("tl").subscribe(0.0); + heartbeatSubscriber = table.getDoubleTopic("hb").subscribe(0.0); txSubscriber = table.getDoubleTopic("tx").subscribe(0.0); tySubscriber = table.getDoubleTopic("ty").subscribe(0.0); megatag1Subscriber = table.getDoubleArrayTopic("botpose_wpiblue").subscribe(new double[] {}); @@ -58,7 +69,7 @@ public VisionIOLimelight(String name, Supplier rotationSupplier) { public void updateInputs(VisionIOInputs inputs) { // Update connection status based on whether an update has been seen in the last 250ms inputs.connected = - ((RobotController.getFPGATime() - latencySubscriber.getLastChange()) / 1000) < 250; + ((RobotController.getFPGATime() - heartbeatSubscriber.getLastChange()) / 1000) < 250; // Update target observation inputs.latestTargetObservation = @@ -68,44 +79,37 @@ public void updateInputs(VisionIOInputs inputs) { // Update orientation for MegaTag 2 orientationPublisher.accept( new double[] {rotationSupplier.get().getDegrees(), 0.0, 0.0, 0.0, 0.0, 0.0}); - NetworkTableInstance.getDefault() - .flush(); // Increases network traffic but recommended by Limelight + flushOrientationIfDue(); // Read new pose observations from NetworkTables Set unionTagIds = new HashSet<>(); - List poseObservations = new LinkedList<>(); + List poseObservations = new ArrayList<>(); for (var rawSample : megatag1Subscriber.readQueue()) { - if (rawSample.value.length == 0) continue; - - int tagCount = (int) rawSample.value[7]; - - // Build used tag array for THIS observation only - int[] used = new int[tagCount]; - int u = 0; + if (!isValidBotPoseSample(rawSample.value)) continue; - for (int i = 11; i < rawSample.value.length && u < tagCount; i += 7) { - int id = (int) rawSample.value[i]; - used[u++] = id; - unionTagIds.add(id); - } + int tagCount = getTagCount(rawSample.value); + int[] used = extractUsedTagIds(rawSample.value); + for (int id : used) unionTagIds.add(id); poseObservations.add( new PoseObservation( // Timestamp, based on server timestamp of publish and latency - rawSample.timestamp * 1.0e-6 - rawSample.value[6] * 1.0e-3, + timestampSeconds(rawSample), // 3D pose estimate parsePose(rawSample.value), // Ambiguity, using only the first tag because ambiguity isn't applicable for multitag - rawSample.value.length >= 18 ? rawSample.value[17] : 0.0, + rawSample.value.length > BOTPOSE_FIRST_TAG_AMBIGUITY_INDEX + ? rawSample.value[BOTPOSE_FIRST_TAG_AMBIGUITY_INDEX] + : 0.0, // Tag count tagCount, // Average tag distance - rawSample.value[9], + rawSample.value[BOTPOSE_AVG_DISTANCE_INDEX], // Observation type PoseObservationType.MEGATAG_1, @@ -115,23 +119,16 @@ public void updateInputs(VisionIOInputs inputs) { } for (var rawSample : megatag2Subscriber.readQueue()) { - if (rawSample.value.length == 0) continue; + if (!isValidBotPoseSample(rawSample.value)) continue; - int tagCount = (int) rawSample.value[7]; - - int[] used = new int[tagCount]; - int u = 0; - - for (int i = 11; i < rawSample.value.length && u < tagCount; i += 7) { - int id = (int) rawSample.value[i]; - used[u++] = id; - unionTagIds.add(id); - } + int tagCount = getTagCount(rawSample.value); + int[] used = extractUsedTagIds(rawSample.value); + for (int id : used) unionTagIds.add(id); poseObservations.add( new PoseObservation( // Timestamp, based on server timestamp of publish and latency - rawSample.timestamp * 1.0e-6 - rawSample.value[6] * 1.0e-3, + timestampSeconds(rawSample), // 3D pose estimate parsePose(rawSample.value), @@ -140,10 +137,10 @@ public void updateInputs(VisionIOInputs inputs) { 0.0, // Tag count - (int) rawSample.value[7], + tagCount, // Average tag distance - rawSample.value[9], + rawSample.value[BOTPOSE_AVG_DISTANCE_INDEX], // Observation type PoseObservationType.MEGATAG_2, @@ -164,7 +161,11 @@ public void updateInputs(VisionIOInputs inputs) { } /** Parses the 3D pose from a Limelight botpose array. */ - private static Pose3d parsePose(double[] rawLLArray) { + static Pose3d parsePose(double[] rawLLArray) { + if (rawLLArray.length < BOTPOSE_MIN_LENGTH) { + throw new IllegalArgumentException( + "Limelight botpose array must have at least " + BOTPOSE_MIN_LENGTH + " values."); + } return new Pose3d( rawLLArray[0], rawLLArray[1], @@ -174,4 +175,45 @@ private static Pose3d parsePose(double[] rawLLArray) { Units.degreesToRadians(rawLLArray[4]), Units.degreesToRadians(rawLLArray[5]))); } + + static boolean isValidBotPoseSample(double[] rawLLArray) { + return rawLLArray.length >= BOTPOSE_MIN_LENGTH + && getTagCount(rawLLArray) >= 0 + && Double.isFinite(rawLLArray[BOTPOSE_LATENCY_INDEX]) + && Double.isFinite(rawLLArray[BOTPOSE_AVG_DISTANCE_INDEX]); + } + + static int[] extractUsedTagIds(double[] rawLLArray) { + int tagCount = getTagCount(rawLLArray); + int[] used = new int[Math.max(0, tagCount)]; + int count = 0; + + for (int i = BOTPOSE_FIRST_TAG_ID_INDEX; + i < rawLLArray.length && count < used.length; + i += BOTPOSE_TAG_STRIDE) { + used[count++] = (int) rawLLArray[i]; + } + + return count == used.length ? used : Arrays.copyOf(used, count); + } + + static double timestampSeconds(TimestampedDoubleArray rawSample) { + return timestampSeconds(rawSample.timestamp, rawSample.value); + } + + static double timestampSeconds(long ntTimestampMicros, double[] rawLLArray) { + return ntTimestampMicros * 1.0e-6 - rawLLArray[BOTPOSE_LATENCY_INDEX] * 1.0e-3; + } + + private static int getTagCount(double[] rawLLArray) { + return (int) rawLLArray[BOTPOSE_TAG_COUNT_INDEX]; + } + + private static void flushOrientationIfDue() { + long nowUs = RobotController.getFPGATime(); + if (nowUs - lastOrientationFlushUs < ORIENTATION_FLUSH_PERIOD_US) return; + + NetworkTableInstance.getDefault().flush(); + lastOrientationFlushUs = nowUs; + } } diff --git a/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVision.java b/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVision.java index 4dac1e05..0763baf7 100644 --- a/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVision.java +++ b/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVision.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.photonvision.PhotonCamera; @@ -50,10 +51,12 @@ public void updateInputs(VisionIOInputs inputs) { Rotation2d bestYaw = Rotation2d.kZero; Rotation2d bestPitch = Rotation2d.kZero; - int processed = 0; - for (var result : camera.getAllUnreadResults()) { - // Hard cap - if (processed++ >= kMaxUnread) break; + List unreadResults = + camera.getAllUnreadResults(); + int startIndex = Math.max(0, unreadResults.size() - kMaxUnread); + + for (int resultIndex = startIndex; resultIndex < unreadResults.size(); resultIndex++) { + var result = unreadResults.get(resultIndex); final double ts = result.getTimestampSeconds(); @@ -64,8 +67,8 @@ public void updateInputs(VisionIOInputs inputs) { } // Add pose observation - if (result.multitagResult.isPresent()) { // Multitag result - var multitag = result.multitagResult.get(); + if (result.getMultiTagResult().isPresent()) { // Multitag result + var multitag = result.getMultiTagResult().get(); // Calculate robot pose Transform3d fieldToCamera = multitag.estimatedPose.best; @@ -74,11 +77,11 @@ public void updateInputs(VisionIOInputs inputs) { // Calculate average tag distance double totalTagDistance = 0.0; - for (var target : result.targets) { - totalTagDistance += target.bestCameraToTarget.getTranslation().getNorm(); + List targets = result.getTargets(); + for (var target : targets) { + totalTagDistance += target.getBestCameraToTarget().getTranslation().getNorm(); } - double avgTagDistance = - result.targets.isEmpty() ? 0.0 : (totalTagDistance / result.targets.size()); + double avgTagDistance = targets.isEmpty() ? 0.0 : (totalTagDistance / targets.size()); // Build used tag list (loggable + replayable) int[] used = new int[multitag.fiducialIDsUsed.size()]; @@ -99,32 +102,32 @@ public void updateInputs(VisionIOInputs inputs) { PoseObservationType.PHOTONVISION, used)); - } else if (!result.targets.isEmpty()) { // Single tag result - var target = result.targets.get(0); + } else if (result.hasTargets()) { // Single tag result + var target = result.getBestTarget(); // Calculate robot pose - var tagPose = aprilTagLayout.getTagPose(target.fiducialId); + var tagPose = aprilTagLayout.getTagPose(target.getFiducialId()); if (tagPose.isEmpty()) continue; Transform3d fieldToTarget = new Transform3d(tagPose.get().getTranslation(), tagPose.get().getRotation()); - Transform3d cameraToTarget = target.bestCameraToTarget; + Transform3d cameraToTarget = target.getBestCameraToTarget(); Transform3d fieldToCamera = fieldToTarget.plus(cameraToTarget.inverse()); Transform3d fieldToRobot = fieldToCamera.plus(robotToCamera.inverse()); Pose3d robotPose = new Pose3d(fieldToRobot.getTranslation(), fieldToRobot.getRotation()); - unionTagIds.add(target.fiducialId); + unionTagIds.add(target.getFiducialId()); poseObservations.add( new PoseObservation( ts, robotPose, - target.poseAmbiguity, + target.getPoseAmbiguity(), 1, cameraToTarget.getTranslation().getNorm(), PoseObservationType.PHOTONVISION, - new int[] {target.fiducialId})); + new int[] {target.getFiducialId()})); } } diff --git a/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVisionSim.java b/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVisionSim.java index f554cc4c..f26e7024 100644 --- a/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVisionSim.java +++ b/src/main/java/frc/robot/subsystems/vision/VisionIOPhotonVisionSim.java @@ -11,6 +11,7 @@ import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.wpilibj.RobotController; import frc.robot.FieldConstants; import java.util.function.Supplier; import org.photonvision.simulation.PhotonCameraSim; @@ -20,12 +21,11 @@ /** IO implementation for physics sim using PhotonVision simulator. */ public class VisionIOPhotonVisionSim extends VisionIOPhotonVision { private static VisionSystemSim visionSim; + private static long lastUpdateUs = Long.MIN_VALUE; + private static final long MIN_UPDATE_PERIOD_US = 1_000; private final Supplier poseSupplier; - @SuppressWarnings("unused") - private final PhotonCameraSim cameraSim; - /** * Creates a new VisionIOPhotonVisionSim. * @@ -35,6 +35,22 @@ public class VisionIOPhotonVisionSim extends VisionIOPhotonVision { */ public VisionIOPhotonVisionSim( String name, Transform3d robotToCamera, Supplier poseSupplier) { + this(name, robotToCamera, new SimCameraProperties(), poseSupplier); + } + + /** + * Creates a new VisionIOPhotonVisionSim. + * + * @param name The name of the camera (PhotonVision camera name). + * @param robotToCamera Camera pose relative to robot frame. + * @param cameraProperties Camera simulation calibration, latency, noise, and FPS. + * @param poseSupplier Supplier for the robot pose (field->robot) to use in simulation. + */ + public VisionIOPhotonVisionSim( + String name, + Transform3d robotToCamera, + SimCameraProperties cameraProperties, + Supplier poseSupplier) { super(name, robotToCamera); this.poseSupplier = poseSupplier; @@ -44,28 +60,23 @@ public VisionIOPhotonVisionSim( visionSim.addAprilTags(FieldConstants.aprilTagLayout); } - // Camera properties: - // - If you have per-camera SimCameraProperties in Constants, pass them here instead. - // - Otherwise keep the default and tune later. - var cameraProperties = new SimCameraProperties(); - - // Recommended defaults (feel free to tune) - // cameraProperties.setCalibration(1280, 800, Rotation2d.fromDegrees(100)); - // cameraProperties.setFPS(20); - // cameraProperties.setAvgLatencyMs(35); - // cameraProperties.setLatencyStdDevMs(5); - - cameraSim = new PhotonCameraSim(camera, cameraProperties); + PhotonCameraSim cameraSim = new PhotonCameraSim(camera, cameraProperties); visionSim.addCamera(cameraSim, robotToCamera); } @Override public void updateInputs(VisionIOInputs inputs) { - // NOTE: This updates the sim world every time a sim camera is polled. - // That's fine (fast enough), but if you want "update once per loop," see note below. - visionSim.update(poseSupplier.get()); + updateVisionSimOncePerLoop(); // Then pull results like normal (and emit PoseObservation + usedTagIds sets) super.updateInputs(inputs); } + + private void updateVisionSimOncePerLoop() { + long nowUs = RobotController.getFPGATime(); + if (nowUs - lastUpdateUs < MIN_UPDATE_PERIOD_US) return; + + visionSim.update(poseSupplier.get()); + lastUpdateUs = nowUs; + } } diff --git a/src/main/java/frc/robot/util/Alert.java b/src/main/java/frc/robot/util/Alert.java index 202012d8..3f64e100 100644 --- a/src/main/java/frc/robot/util/Alert.java +++ b/src/main/java/frc/robot/util/Alert.java @@ -22,7 +22,7 @@ /** Class for managing persistent alerts to be sent over NetworkTables. */ public class Alert { - private static Map groups = new HashMap(); + private static final Map groups = new HashMap<>(); private final AlertType type; private boolean active = false; @@ -105,7 +105,7 @@ private static class SendableAlerts implements Sendable { public String[] getStrings(AlertType type) { Predicate activeFilter = (Alert x) -> x.type == type && x.active; Comparator timeSorter = - (Alert a1, Alert a2) -> (int) (a2.activeStartTime - a1.activeStartTime); + (Alert a1, Alert a2) -> Double.compare(a2.activeStartTime, a1.activeStartTime); return alerts.stream() .filter(activeFilter) .sorted(timeSorter) diff --git a/src/main/java/frc/robot/util/ConcurrentTimeInterpolatableBuffer.java b/src/main/java/frc/robot/util/ConcurrentTimeInterpolatableBuffer.java index a2e55e61..aa9a091c 100644 --- a/src/main/java/frc/robot/util/ConcurrentTimeInterpolatableBuffer.java +++ b/src/main/java/frc/robot/util/ConcurrentTimeInterpolatableBuffer.java @@ -22,7 +22,9 @@ import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.interpolation.Interpolatable; import edu.wpi.first.math.interpolation.Interpolator; +import java.util.Collections; import java.util.Map.Entry; +import java.util.NavigableMap; import java.util.Optional; import java.util.OptionalDouble; import java.util.concurrent.ConcurrentNavigableMap; @@ -150,8 +152,8 @@ public Optional getSample(double timeSeconds) { m_interpolatingFunc.interpolate(bottomBound.getValue(), topBound.getValue(), ratio)); } - public Entry getLatest() { - return m_pastSnapshots.lastEntry(); + public Optional> getLatest() { + return Optional.ofNullable(m_pastSnapshots.lastEntry()); } /** @@ -160,6 +162,25 @@ public Entry getLatest() { * * @return The internal sample buffer. */ + public NavigableMap getSamplesInRange( + double startTimeSeconds, + boolean startInclusive, + double endTimeSeconds, + boolean endInclusive) { + if (endTimeSeconds < startTimeSeconds) { + return Collections.emptyNavigableMap(); + } + return Collections.unmodifiableNavigableMap( + m_pastSnapshots.subMap(startTimeSeconds, startInclusive, endTimeSeconds, endInclusive)); + } + + /** + * Grant access to the internal sample buffer. Prefer {@link #getSamplesInRange} for read-only + * access. + * + * @return The internal sample buffer. + */ + @Deprecated(forRemoval = false) public ConcurrentNavigableMap getInternalBuffer() { return m_pastSnapshots; } diff --git a/src/main/java/frc/robot/util/GetJoystickValue.java b/src/main/java/frc/robot/util/GetJoystickValue.java deleted file mode 100644 index c149845d..00000000 --- a/src/main/java/frc/robot/util/GetJoystickValue.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2024-2026 Az-FIRST -// http://github.com/AZ-First -// -// Use of this source code is governed by a BSD -// license that can be found in the AdvantageKit-License.md file -// at the root directory of this project. - -package frc.robot.util; - -/** - * Interface needed to abstraxct away which joystick is used for driving and which for steering with - * a swerve base. Teams may specify to use the left joystick for either driving or steering in the - * `Constants.java` file under OperatorConstants. - */ -@FunctionalInterface -public interface GetJoystickValue { - double value(); -} diff --git a/src/main/java/frc/robot/util/LocalADStarAK.java b/src/main/java/frc/robot/util/LocalADStarAK.java index 36ec03b8..e4353ade 100644 --- a/src/main/java/frc/robot/util/LocalADStarAK.java +++ b/src/main/java/frc/robot/util/LocalADStarAK.java @@ -138,7 +138,7 @@ public void fromLog(LogTable table) { double[] pointsLogged = table.get("CurrentPathPoints", new double[0]); List pathPoints = new ArrayList<>(); - for (int i = 0; i < pointsLogged.length; i += 2) { + for (int i = 0; i + 1 < pointsLogged.length; i += 2) { pathPoints.add( new PathPoint(new Translation2d(pointsLogged[i], pointsLogged[i + 1]), null)); } diff --git a/src/main/java/frc/robot/util/LoggedTunableNumber.java b/src/main/java/frc/robot/util/LoggedTunableNumber.java index d4cb549b..b9c7b1b6 100644 --- a/src/main/java/frc/robot/util/LoggedTunableNumber.java +++ b/src/main/java/frc/robot/util/LoggedTunableNumber.java @@ -18,31 +18,33 @@ import org.littletonrobotics.junction.networktables.LoggedNetworkNumber; /** - * Class for a tunable number. Gets value from dashboard in tuning mode, returns default if not or - * value not in dashboard. + * Replay-safe tunable number backed by AdvantageKit NetworkTables inputs. + * + *

When tuning mode is enabled, the live value is read from {@code /Tuning/...}. Otherwise, the + * default value is returned. */ public class LoggedTunableNumber implements DoubleSupplier { - private static final String tableKey = "TunableNumbers"; + private static final String tableKey = "/Tuning"; private final String key; private boolean hasDefault = false; private double defaultValue; private LoggedNetworkNumber dashboardNumber; - private Map lastHasChangedValues = new HashMap<>(); + private final Map lastHasChangedValues = new HashMap<>(); /** - * Create a new LoggedTunableNumber + * Create a new LoggedTunableNumber. * - * @param dashboardKey Key on dashboard + * @param dashboardKey Key under /Tuning */ public LoggedTunableNumber(String dashboardKey) { this.key = tableKey + "/" + dashboardKey; } /** - * Create a new LoggedTunableNumber with the default value + * Create a new LoggedTunableNumber with a default value. * - * @param dashboardKey Key on dashboard + * @param dashboardKey Key under /Tuning * @param defaultValue Default value */ public LoggedTunableNumber(String dashboardKey, double defaultValue) { @@ -51,45 +53,50 @@ public LoggedTunableNumber(String dashboardKey, double defaultValue) { } /** - * Set the default value of the number. The default value can only be set once. + * Set the default value. Can only be set once. * * @param defaultValue The default value */ public void initDefault(double defaultValue) { - if (!hasDefault) { - hasDefault = true; - this.defaultValue = defaultValue; - if (Constants.tuningMode) { - dashboardNumber = new LoggedNetworkNumber("SmartDashboard/" + key, defaultValue); - } + if (hasDefault) { + return; + } + + hasDefault = true; + this.defaultValue = defaultValue; + + if (isTuningMode()) { + dashboardNumber = new LoggedNetworkNumber(key, defaultValue); } } /** - * Get the current value, from dashboard if available and in tuning mode. + * Get the current value. * - * @return The current value + * @return Live tuning value in tuning mode, otherwise the default */ public double get() { if (!hasDefault) { return 0.0; - } else { - return Constants.tuningMode ? dashboardNumber.get() : defaultValue; } + + if (dashboardNumber != null) { + return dashboardNumber.get(); + } + + return defaultValue; } /** - * Checks whether the number has changed since our last check + * Returns true when this value has changed since the last check by the given caller. * - * @param id Unique identifier for the caller to avoid conflicts when shared between multiple - * objects. Recommended approach is to pass the result of "hashCode()" - * @return True if the number has changed since the last time this method was called, false - * otherwise. + * @param id Unique identifier for the caller, often hashCode() */ public boolean hasChanged(int id) { double currentValue = get(); Double lastValue = lastHasChangedValues.get(id); - if (lastValue == null || currentValue != lastValue) { + + if (lastValue == null || Double.compare(currentValue, lastValue) != 0) { lastHasChangedValues.put(id, currentValue); return true; } @@ -98,22 +105,20 @@ public boolean hasChanged(int id) { } /** - * Runs action if any of the tunableNumbers have changed + * Runs an action if any of the tunable numbers changed. * - * @param id Unique identifier for the caller to avoid conflicts when shared between multiple * - * objects. Recommended approach is to pass the result of "hashCode()" - * @param action Callback to run when any of the tunable numbers have changed. Access tunable - * numbers in order inputted in method - * @param tunableNumbers All tunable numbers to check + * @param id Unique identifier for the caller, often hashCode() + * @param action Callback with values in the same order as provided + * @param tunableNumbers Tunable numbers to watch */ public static void ifChanged( int id, Consumer action, LoggedTunableNumber... tunableNumbers) { - if (Arrays.stream(tunableNumbers).anyMatch(tunableNumber -> tunableNumber.hasChanged(id))) { + if (Arrays.stream(tunableNumbers).anyMatch(t -> t.hasChanged(id))) { action.accept(Arrays.stream(tunableNumbers).mapToDouble(LoggedTunableNumber::get).toArray()); } } - /** Runs action if any of the tunableNumbers have changed */ + /** Runs an action if any of the tunable numbers changed. */ public static void ifChanged(int id, Runnable action, LoggedTunableNumber... tunableNumbers) { ifChanged(id, values -> action.run(), tunableNumbers); } @@ -122,4 +127,8 @@ public static void ifChanged(int id, Runnable action, LoggedTunableNumber... tun public double getAsDouble() { return get(); } + + private static boolean isTuningMode() { + return Constants.isTuningMode(); + } } diff --git a/src/main/java/frc/robot/util/OverrideSwitches.java b/src/main/java/frc/robot/util/OverrideSwitches.java index 755a66f9..cc12fa91 100644 --- a/src/main/java/frc/robot/util/OverrideSwitches.java +++ b/src/main/java/frc/robot/util/OverrideSwitches.java @@ -40,7 +40,7 @@ public boolean isConnected() { /** Gets the state of a driver-side switch (0-2 from left to right). */ public boolean getDriverSwitch(int index) { if (index < 0 || index > 2) { - throw new RuntimeException( + throw new IllegalArgumentException( "Invalid driver override index " + Integer.toString(index) + ". Must be 0-2."); } return consoleSwitches.getRawButton(index + 1); @@ -49,7 +49,7 @@ public boolean getDriverSwitch(int index) { /** Gets the state of an operator-side switch (0-4 from left to right). */ public boolean getOperatorSwitch(int index) { if (index < 0 || index > 4) { - throw new RuntimeException( + throw new IllegalArgumentException( "Invalid operator override index " + Integer.toString(index) + ". Must be 0-4."); } return consoleSwitches.getRawButton(index + 8); diff --git a/src/main/java/frc/robot/util/RBSICANHealth.java b/src/main/java/frc/robot/util/RBSICANHealth.java index 8959020f..0da22e9c 100644 --- a/src/main/java/frc/robot/util/RBSICANHealth.java +++ b/src/main/java/frc/robot/util/RBSICANHealth.java @@ -29,6 +29,11 @@ public RBSICANHealth(String busName) { bus = RBSICANBusRegistry.getLike(busName); } + @Override + protected int getPeriodPriority() { + return 20; + } + /** Periodic function */ @Override protected void rbsiPeriodic() { diff --git a/src/main/java/frc/robot/util/RBSIIO.java b/src/main/java/frc/robot/util/RBSIIO.java index ae72aba0..7bcdd640 100644 --- a/src/main/java/frc/robot/util/RBSIIO.java +++ b/src/main/java/frc/robot/util/RBSIIO.java @@ -19,26 +19,28 @@ */ public interface RBSIIO { - /** Define the power ports for this subsystem */ - public final int[] powerPorts = {}; + /** Implementations may override to provide PDH ports. */ + default int[] powerPorts() { + return new int[] {}; + } + + /** Return the list of PDH power ports used for this mechanism. */ + default int[] getPowerPorts() { + return powerPorts(); + } /** Stop all the motors */ - public default void stop() {} + default void stop() {} /** Set the neutral mode of the motors to COAST */ - public default void setCoast() {} + default void setCoast() {} /** Set the neutral mode of the motors to BRAKE */ - public default void setBrake() {} + default void setBrake() {} /** Run open loop at the specified voltage */ - public default void setVoltage(double volts) {} + default void setVoltage(double volts) {} /** Run open loop at the specified duty cycle */ - public default void setPercent(double percent) {} - - /** Return the list of PDH power ports used for this mechanism */ - public default int[] getPowerPorts() { - return powerPorts; - } + default void setPercent(double percent) {} } diff --git a/src/main/java/frc/robot/util/RBSIParsing.java b/src/main/java/frc/robot/util/RBSIParsing.java index 0215ff2c..4b41b8dc 100644 --- a/src/main/java/frc/robot/util/RBSIParsing.java +++ b/src/main/java/frc/robot/util/RBSIParsing.java @@ -27,9 +27,9 @@ public class RBSIParsing { */ public static Byte parseModuleType() { // NOTE: This assumes all 4 modules have the same arrangement! - Byte b_drive; // [x,x,-,-,-,-,-,-] - Byte b_steer; // [-,-,x,x,-,-,-,-] - Byte b_encoder; // [-,-,-,-,x,x,-,-] + byte b_drive; // [x,x,-,-,-,-,-,-] + byte b_steer; // [-,-,x,x,-,-,-,-] + byte b_encoder; // [-,-,-,-,x,x,-,-] switch (kFLDriveType) { case "falcon": case "kraken": @@ -43,7 +43,7 @@ public static Byte parseModuleType() { b_drive = 0b01; break; default: - throw new RuntimeException("Invalid drive motor type"); + throw new IllegalArgumentException("Invalid drive motor type: " + kFLDriveType); } switch (kFLSteerType) { case "falcon": @@ -58,7 +58,7 @@ public static Byte parseModuleType() { b_steer = 0b01; break; default: - throw new RuntimeException("Invalid steer motor type"); + throw new IllegalArgumentException("Invalid steer motor type: " + kFLSteerType); } switch (kFLEncoderType) { case "cancoder": @@ -70,7 +70,7 @@ public static Byte parseModuleType() { b_encoder = 0b01; break; default: - throw new RuntimeException("Invalid swerve encoder type"); + throw new IllegalArgumentException("Invalid swerve encoder type: " + kFLEncoderType); } return (byte) (0b00000000 | b_drive << 6 | b_steer << 4 | b_encoder << 2); } diff --git a/src/main/java/frc/robot/util/RBSIPowerMonitor.java b/src/main/java/frc/robot/util/RBSIPowerMonitor.java index ba6daadf..6c94945a 100644 --- a/src/main/java/frc/robot/util/RBSIPowerMonitor.java +++ b/src/main/java/frc/robot/util/RBSIPowerMonitor.java @@ -29,14 +29,17 @@ public class RBSIPowerMonitor extends VirtualSubsystem { private final RBSISubsystem[] subsystems; - // private final LoggedPowerDistribution m_pdm = - // LoggedPowerDistribution.getInstance(PowerConstants.kPDMCANid, PowerConstants.kPDMType); - ConduitApi conduit = ConduitApi.getInstance(); + private final ConduitApi conduit = ConduitApi.getInstance(); // Define local variables private final LoggedTunableNumber batteryCapacityAh; private double totalAmpHours = 0.0; + private double totalEnergyJoules = 0.0; private long lastTimestampUs = RobotController.getFPGATime(); // In microseconds + private double lastVoltage = 0.0; + private double lastTotalCurrent = 0.0; + private boolean totalCurrentOverLimit = false; + private boolean brownoutImminent = false; // DRIVE and STEER motor power ports private final int[] m_drivePowerPorts = { @@ -71,6 +74,11 @@ public RBSIPowerMonitor(LoggedTunableNumber batteryCapacityAh, RBSISubsystem... } } + @Override + protected int getPeriodPriority() { + return 30; + } + /** Periodic Method */ @Override public void rbsiPeriodic() { @@ -80,14 +88,21 @@ public void rbsiPeriodic() { // --- Read voltage & total current --- double voltage = conduit.getPDPVoltage(); double totalCurrent = conduit.getPDPTotalCurrent(); + lastVoltage = voltage; + lastTotalCurrent = totalCurrent; + totalCurrentOverLimit = totalCurrent > PowerConstants.kTotalMaxCurrentAmps; // --- Safety alerts --- - totalCurrentAlert.set(totalCurrent > PowerConstants.kTotalMaxCurrent); - lowVoltageAlert.set(voltage < PowerConstants.kVoltageWarning); - criticalVoltageAlert.set(voltage < PowerConstants.kVoltageCritical); - - for (int ch = 0; ch < conduit.getPDPChannelCount(); ch++) { - portAlerts[ch].set(conduit.getPDPChannelCurrent(ch) > PowerConstants.kMotorPortMaxCurrent); + totalCurrentAlert.set(totalCurrentOverLimit); + lowVoltageAlert.set(voltage < PowerConstants.kWarningVoltage); + criticalVoltageAlert.set(voltage < PowerConstants.kCriticalVoltage); + Logger.recordOutput("Power/Voltage", voltage); + Logger.recordOutput("Power/TotalCurrent", totalCurrent); + Logger.recordOutput("Power/TotalCurrentOverLimit", totalCurrentOverLimit); + + for (int ch = 0; ch < Math.min(conduit.getPDPChannelCount(), portAlerts.length); ch++) { + portAlerts[ch].set( + conduit.getPDPChannelCurrent(ch) > PowerConstants.kMotorPortMaxCurrentAmps); } // --- Battery estimation --- @@ -96,10 +111,11 @@ public void rbsiPeriodic() { lastTimestampUs = nowUs; totalAmpHours += totalCurrent * dtSec / 3600.0; // accumulate amp-hours + double capacityAh = batteryCapacityAh.getAsDouble(); double batteryPercent = - 100.0 * (batteryCapacityAh.getAsDouble() - totalAmpHours) / batteryCapacityAh.getAsDouble(); + capacityAh > 0.0 ? 100.0 * (capacityAh - totalAmpHours) / capacityAh : 0.0; - Logger.recordOutput("Power/BatteryPercentEstimate", batteryPercent); + Logger.recordOutput("Power/BatteryPercentEstimate", Math.max(0.0, batteryPercent)); Logger.recordOutput("Power/AmpHoursUsed", totalAmpHours); // --- Drive & Steer aggregation --- @@ -113,28 +129,49 @@ public void rbsiPeriodic() { // --- Energy / power calculations --- double totalPower = voltage * totalCurrent; // Watts + totalEnergyJoules += totalPower * dtSec; Logger.recordOutput("Power/TotalPower", totalPower); - Logger.recordOutput("Power/EnergyJoules", totalPower * dtSec); - Logger.recordOutput("Power/EnergyWh", totalPower * dtSec / 3600.0); + Logger.recordOutput("Power/EnergyJoules", totalEnergyJoules); + Logger.recordOutput("Power/EnergyWh", totalEnergyJoules / 3600.0); // --- Brownout prediction --- - boolean brownoutImminent = voltage < PowerConstants.kVoltageLimiting; + brownoutImminent = voltage < PowerConstants.kLimitingVoltage; Logger.recordOutput("Power/BrownoutImminent", brownoutImminent); - - // --- Optional hooks for current shedding --- - if (brownoutImminent) { - // TODO: implement automatic shedding: e.g., disable non-critical subsystems - } } private void logGroupCurrent(String name, int[] ports) { double sum = 0.0; for (int port : ports) { - sum += conduit.getPDPChannelCurrent(port); + if (port >= 0 && port < conduit.getPDPChannelCount()) { + sum += conduit.getPDPChannelCurrent(port); + } } - Logger.recordOutput("Power/Subsystems/" + name + "Current", sum); + Logger.recordOutput("Power/Subsystems/" + name + "_Current", sum); } - // TODO: Do something about setting priorities if drawing too much current + /** Returns the most recently sampled battery voltage. */ + public double getLastVoltage() { + return lastVoltage; + } + + /** Returns the most recently sampled total current draw. */ + public double getLastTotalCurrent() { + return lastTotalCurrent; + } + /** Returns whether the last sampled total current exceeded the configured warning limit. */ + public boolean isTotalCurrentOverLimit() { + return totalCurrentOverLimit; + } + + /** + * Returns whether the last sampled voltage is below the limiting threshold. + * + *

Mechanism-specific commands can use this signal to shed load intentionally. The generic + * power monitor should not stop motors by itself because mechanism priority is game- and + * robot-specific. + */ + public boolean isBrownoutImminent() { + return brownoutImminent; + } } diff --git a/src/main/java/frc/robot/util/RBSISubsystem.java b/src/main/java/frc/robot/util/RBSISubsystem.java index b90752fb..52e79140 100644 --- a/src/main/java/frc/robot/util/RBSISubsystem.java +++ b/src/main/java/frc/robot/util/RBSISubsystem.java @@ -22,7 +22,11 @@ * added functionality. */ public abstract class RBSISubsystem extends SubsystemBase { + private static final int TIMING_LOG_PERIOD_LOOPS = 5; + private static final int[] NO_POWER_PORTS = {}; + private final String name = getClass().getSimpleName(); + private int timingLogLoops = 0; /** * Guaranteed timing wrapper (cannot be bypassed by subclasses). @@ -38,8 +42,11 @@ public abstract class RBSISubsystem extends SubsystemBase { public final void periodic() { long start = System.nanoTime(); rbsiPeriodic(); - // Log the timing for this subsystem - Logger.recordOutput("LogPeriodic/Subsystem/" + name + "MS", (System.nanoTime() - start) / 1e6); + if (++timingLogLoops >= TIMING_LOG_PERIOD_LOOPS) { + timingLogLoops = 0; + Logger.recordOutput( + "LogPeriodic/Subsystem/" + name + "MS", (System.nanoTime() - start) / 1e6); + } } /** Subclasses must implement this instead of periodic(). */ @@ -51,7 +58,6 @@ public final void periodic() { * @return Array of power distribution module ports */ public int[] getPowerPorts() { - int[] retval = {}; - return retval; + return NO_POWER_PORTS; } } diff --git a/src/main/java/frc/robot/util/RobotDeviceId.java b/src/main/java/frc/robot/util/RobotDeviceId.java index e51ccba1..94db5815 100644 --- a/src/main/java/frc/robot/util/RobotDeviceId.java +++ b/src/main/java/frc/robot/util/RobotDeviceId.java @@ -16,6 +16,7 @@ package frc.robot.util; import com.ctre.phoenix6.CANBus; +import java.util.Objects; /** * Class for wrapping Robot / CAN devices with a name and functionality. Included here are both the @@ -50,17 +51,34 @@ public String getBus() { /** Get the CTRE CANBus object for a named device */ public CANBus getCANBus() { + return RBSICANBusRegistry.getBus(m_CANBus); + } - return new CANBus(m_CANBus); + /** Returns whether this device has a configured Power Distribution channel. */ + public boolean hasPowerPort() { + return m_PowerPort != null; } /** Get the Power Port for a named device */ public int getPowerPort() { + if (m_PowerPort == null) { + throw new IllegalStateException( + "Device " + m_CANDeviceNumber + " on CAN bus '" + m_CANBus + "' has no power port."); + } return m_PowerPort; } /** Check whether two named devices are, in fact, the same */ - public boolean equals(RobotDeviceId other) { - return other.m_CANDeviceNumber == m_CANDeviceNumber && other.m_CANBus == m_CANBus; + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof RobotDeviceId otherDevice)) return false; + return otherDevice.m_CANDeviceNumber == m_CANDeviceNumber + && Objects.equals(otherDevice.m_CANBus, m_CANBus); + } + + @Override + public int hashCode() { + return Objects.hash(m_CANDeviceNumber, m_CANBus); } } diff --git a/src/main/java/frc/robot/util/TimeUtil.java b/src/main/java/frc/robot/util/TimeUtil.java index 62f3cb39..c0c4d205 100644 --- a/src/main/java/frc/robot/util/TimeUtil.java +++ b/src/main/java/frc/robot/util/TimeUtil.java @@ -21,13 +21,6 @@ private TimeUtil() {} /** Always seconds, regardless of real/sim/replay timebase quirks. */ public static double now() { - double t = Logger.getTimestamp(); - - // Empirical: in some environments, Logger timestamp is microseconds. - // If it looks like µs, convert to seconds. - if (t > 1.0e6) { - t *= 1.0e-6; - } - return t; + return Logger.getTimestamp() * 1.0e-6; } } diff --git a/src/main/java/frc/robot/util/VirtualSubsystem.java b/src/main/java/frc/robot/util/VirtualSubsystem.java index bb99b7ea..f0783884 100644 --- a/src/main/java/frc/robot/util/VirtualSubsystem.java +++ b/src/main/java/frc/robot/util/VirtualSubsystem.java @@ -18,13 +18,18 @@ * Base class for virtual subsystems -- not robot hardware -- that should be treated as subsystems */ public abstract class VirtualSubsystem { + private static final int TIMING_LOG_PERIOD_LOOPS = 5; private static final List subsystems = new ArrayList<>(); private static boolean needsSort = false; + private static long nextConstructionOrder = 0; private final String name = getClass().getSimpleName(); + private final long constructionOrder; + private int timingLogLoops = 0; // Load all defined virtual subsystems into a list public VirtualSubsystem() { + constructionOrder = nextConstructionOrder++; subsystems.add(this); needsSort = true; // a new subsystem changed ordering } @@ -32,7 +37,7 @@ public VirtualSubsystem() { /** * Override to control ordering. Lower runs earlier. * - *

Example: IMU inputs -30, Drive odometry -20, Vision -10, Coordinator 0. + *

Example: IMU inputs -30, Drive odometry -20, Vision -10, telemetry/health monitors +20. */ protected int getPeriodPriority() { return 0; @@ -47,8 +52,8 @@ public static void periodicAll() { if (needsSort) { subsystems.sort( Comparator.comparingInt(VirtualSubsystem::getPeriodPriority) - // deterministic tie-break to avoid “random” order when priorities match - .thenComparingInt(System::identityHashCode)); + // Preserve construction order when priorities match. + .thenComparingLong(subsystem -> subsystem.constructionOrder)); needsSort = false; } @@ -72,11 +77,19 @@ public static void periodicAll() { public final void periodic() { long start = System.nanoTime(); rbsiPeriodic(); - // Log the timing for this subsystem - Logger.recordOutput( - "LogPeriodic/VirtualSubsystem/" + name + "MS", (System.nanoTime() - start) / 1e6); + if (++timingLogLoops >= TIMING_LOG_PERIOD_LOOPS) { + timingLogLoops = 0; + Logger.recordOutput( + "LogPeriodic/VirtualSubsystem/" + name + "MS", (System.nanoTime() - start) / 1e6); + } } /** Subclasses must implement this instead of periodic(). */ protected abstract void rbsiPeriodic(); + + static void resetForTesting() { + subsystems.clear(); + needsSort = false; + nextConstructionOrder = 0; + } } diff --git a/src/main/java/frc/robot/util/YagslConstants.java b/src/main/java/frc/robot/util/YagslConstants.java index 1542f85d..efd43942 100644 --- a/src/main/java/frc/robot/util/YagslConstants.java +++ b/src/main/java/frc/robot/util/YagslConstants.java @@ -33,11 +33,12 @@ public class YagslConstants { // Define the internal YAGSL objects we will read into private static final File yagslDir = - new File(Filesystem.getDeployDirectory(), DeployConstants.yagslDir); + new File(Filesystem.getDeployDirectory(), DeployConstants.kYagslDir); public static final SwerveDriveJson swerveDriveJson; // Needed by the Accelerometer subsystem private static final PIDFPropertiesJson pidfPropertiesJson; private static final PhysicalPropertiesJson physicalPropertiesJson; private static final ModuleJson[] moduleJsons; + private static final ObjectMapper mapper = new ObjectMapper(); // Use a static to read in the YAGSL JSON files with error catching static { @@ -48,17 +49,13 @@ public class YagslConstants { try { tempSwerveJson = - new ObjectMapper() - .readValue(new File(yagslDir, "swervedrive.json"), SwerveDriveJson.class); + mapper.readValue(new File(yagslDir, "swervedrive.json"), SwerveDriveJson.class); tempPdifJson = - new ObjectMapper() - .readValue( - new File(yagslDir, "modules/pidfproperties.json"), PIDFPropertiesJson.class); + mapper.readValue( + new File(yagslDir, "modules/pidfproperties.json"), PIDFPropertiesJson.class); tempPhysicalJson = - new ObjectMapper() - .readValue( - new File(yagslDir, "modules/physicalproperties.json"), - PhysicalPropertiesJson.class); + mapper.readValue( + new File(yagslDir, "modules/physicalproperties.json"), PhysicalPropertiesJson.class); } catch (Exception e) { throw new RuntimeException(e); @@ -72,8 +69,10 @@ public class YagslConstants { for (int i = 0; i < tempModuleJsons.length; i++) { // moduleConfigs.put(swerveDriveJson.modules[i], i); File moduleFile = new File(yagslDir, "modules/" + swerveDriveJson.modules[i]); - assert moduleFile.exists(); - tempModuleJsons[i] = new ObjectMapper().readValue(moduleFile, ModuleJson.class); + if (!moduleFile.exists()) { + throw new IllegalStateException("Missing YAGSL module config: " + moduleFile); + } + tempModuleJsons[i] = mapper.readValue(moduleFile, ModuleJson.class); } } catch (Exception e) { throw new RuntimeException(e); @@ -111,7 +110,7 @@ public class YagslConstants { static List moduleList = Arrays.asList(swerveDriveJson.modules); // Front Left - static ModuleJson flModule = moduleJsons[moduleList.indexOf("frontleft.json")]; + static ModuleJson flModule = moduleJsons[moduleIndex("frontleft.json")]; public static final int kFrontLeftDriveMotorId = flModule.drive.id; public static final int kFrontLeftSteerMotorId = flModule.angle.id; public static final int kFrontLeftEncoderId = flModule.encoder.id; @@ -129,7 +128,7 @@ public class YagslConstants { public static final double kFrontLeftYPosInches = flModule.location.front; // Front Right - static ModuleJson frModule = moduleJsons[moduleList.indexOf("frontright.json")]; + static ModuleJson frModule = moduleJsons[moduleIndex("frontright.json")]; public static final int kFrontRightDriveMotorId = frModule.drive.id; public static final int kFrontRightSteerMotorId = frModule.angle.id; public static final int kFrontRightEncoderId = frModule.encoder.id; @@ -147,7 +146,7 @@ public class YagslConstants { public static final double kFrontRightYPosInches = frModule.location.front; // Back Left - static ModuleJson blModule = moduleJsons[moduleList.indexOf("backleft.json")]; + static ModuleJson blModule = moduleJsons[moduleIndex("backleft.json")]; public static final int kBackLeftDriveMotorId = blModule.drive.id; public static final int kBackLeftSteerMotorId = blModule.angle.id; public static final int kBackLeftEncoderId = blModule.encoder.id; @@ -165,7 +164,7 @@ public class YagslConstants { public static final double kBackLeftYPosInches = blModule.location.front; // Back Right - static ModuleJson brModule = moduleJsons[moduleList.indexOf("backright.json")]; + static ModuleJson brModule = moduleJsons[moduleIndex("backright.json")]; public static final int kBackRightDriveMotorId = brModule.drive.id; public static final int kBackRightSteerMotorId = brModule.angle.id; public static final int kBackRightEncoderId = brModule.encoder.id; @@ -193,4 +192,13 @@ public class YagslConstants { public static final double kSteerD = pidfPropertiesJson.angle.d; public static final double kSteerF = pidfPropertiesJson.angle.f; public static final double kSteerIZ = pidfPropertiesJson.angle.iz; + + private static int moduleIndex(String fileName) { + int index = moduleList.indexOf(fileName); + if (index < 0) { + throw new IllegalStateException( + "YAGSL swervedrive.json is missing required module config '" + fileName + "'."); + } + return index; + } } diff --git a/src/test/CurrentLimitTests.java b/src/test/CurrentLimitTests.java index 079ec0ea..7f686d65 100644 --- a/src/test/CurrentLimitTests.java +++ b/src/test/CurrentLimitTests.java @@ -14,11 +14,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.ctre.phoenix6.StatusCode; +import com.ctre.phoenix6.StatusSignal; import com.ctre.phoenix6.configs.CurrentLimitsConfigs; import com.ctre.phoenix6.configs.TalonFXConfiguration; import com.ctre.phoenix6.controls.DutyCycleOut; import com.ctre.phoenix6.hardware.TalonFX; import edu.wpi.first.hal.HAL; +import edu.wpi.first.units.measure.Current; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.simulation.DriverStationSim; @@ -81,10 +83,10 @@ public void testStatorLimit() { Timer.delay(0.020); /* Get the next update for stator current */ - statorCurrent.waitForUpdate(1); + double initialCurrent = waitForCurrentAbove(statorCurrent, 100.0, 1.0); - System.out.println("Stator current is " + statorCurrent); - assertTrue(statorCurrent.getValue() > 100); // Stator current should be in excess of 100 amps + System.out.println("Stator current is " + initialCurrent + " A"); + assertTrue(initialCurrent > 100); // Stator current should be in excess of 100 amps /* Now apply the stator current limit */ currentLimitConfigs.StatorCurrentLimitEnable = true; @@ -97,7 +99,7 @@ public void testStatorLimit() { statorCurrent.waitForUpdate(1); System.out.println("Stator current is " + statorCurrent); - assertTrue(statorCurrent.getValue() < 25); // Give some wiggle room + assertTrue(statorCurrent.getValueAsDouble() < 25); // Give some wiggle room } @Test @@ -107,10 +109,10 @@ public void testSupplyLimit() { /* Configure a supply limit of 20 amps */ TalonFXConfiguration toConfigure = new TalonFXConfiguration(); CurrentLimitsConfigs currentLimitConfigs = toConfigure.CurrentLimits; - currentLimitConfigs.SupplyCurrentLimit = 5; - currentLimitConfigs.SupplyCurrentThreshold = 10; - currentLimitConfigs.SupplyTimeThreshold = 1.0; - currentLimitConfigs.StatorCurrentLimitEnable = false; // Start with supply limits off + currentLimitConfigs.SupplyCurrentLimit = 10; + currentLimitConfigs.SupplyCurrentLowerLimit = 5; + currentLimitConfigs.SupplyCurrentLowerTime = 1.0; + currentLimitConfigs.SupplyCurrentLimitEnable = false; // Start with supply limits off retryConfigApply(() -> talon.getConfigurator().apply(toConfigure)); @@ -123,7 +125,8 @@ public void testSupplyLimit() { supplyCurrent.waitForUpdate(1); System.out.println("Supply current is " + supplyCurrent); - assertTrue(supplyCurrent.getValue() > 80); // Supply current should be in excess of 80 amps + assertTrue( + supplyCurrent.getValueAsDouble() > 25); // Supply current should be high before limiting /* Now apply the supply current limit */ currentLimitConfigs.SupplyCurrentLimitEnable = true; @@ -137,8 +140,9 @@ public void testSupplyLimit() { System.out.println("Supply current is " + supplyCurrent); assertTrue( - supplyCurrent.getValue() - > 80); // Make sure it's still over 80 amps (time hasn't exceeded 1 second in total) + supplyCurrent.getValueAsDouble() <= 15 + && supplyCurrent.getValueAsDouble() + > 5); // Time has not exceeded the lower-limit threshold yet /* Wait a full extra couple seconds so the limit kicks in and starts limiting us */ Timer.delay(2); @@ -147,7 +151,7 @@ public void testSupplyLimit() { supplyCurrent.waitForUpdate(1); System.out.println("Supply current is " + supplyCurrent); - assertTrue(supplyCurrent.getValue() < 10); // Give some wiggle room + assertTrue(supplyCurrent.getValueAsDouble() < 10); // Give some wiggle room } private void retryConfigApply(Supplier toApply) { @@ -156,6 +160,17 @@ private void retryConfigApply(Supplier toApply) { do { finalCode = toApply.get(); } while (!finalCode.isOK() && --triesLeftOver > 0); - assert (finalCode.isOK()); + assertTrue(finalCode.isOK(), "Config apply failed: " + finalCode); + } + + private double waitForCurrentAbove( + StatusSignal currentSignal, double threshold, double timeoutSec) { + double current = currentSignal.getValueAsDouble(); + double startTime = Timer.getFPGATimestamp(); + while (current <= threshold && Timer.getFPGATimestamp() - startTime < timeoutSec) { + currentSignal.waitForUpdate(0.100); + current = currentSignal.getValueAsDouble(); + } + return current; } } diff --git a/src/test/FusedCANcoderTests.java b/src/test/FusedCANcoderTests.java index c77f4c03..2bc7eb03 100644 --- a/src/test/FusedCANcoderTests.java +++ b/src/test/FusedCANcoderTests.java @@ -12,6 +12,7 @@ // the WPILib BSD license file in the root directory of this project. import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.ctre.phoenix6.BaseStatusSignal; import com.ctre.phoenix6.StatusCode; @@ -74,8 +75,8 @@ public void testIndividualPos() { System.out.println("Talon Pos vs expected: " + talonPos + " vs " + TALON_POSITION); System.out.println("CANcoder Pos vs expected: " + cancoderPos + " vs " + CANCODER_POSITION); - assertEquals(talonPos.getValue(), TALON_POSITION, SET_DELTA); - assertEquals(cancoderPos.getValue(), CANCODER_POSITION, SET_DELTA); + assertEquals(talonPos.getValueAsDouble(), TALON_POSITION, SET_DELTA); + assertEquals(cancoderPos.getValueAsDouble(), CANCODER_POSITION, SET_DELTA); } @Test @@ -119,8 +120,8 @@ public void testFusedCANcoderPos() { /* Make sure Talon matches CANcoder, since it should be using CANcoder's position */ System.out.println("Talon Pos vs expected: " + talonPos + " vs " + CANCODER_POSITION); System.out.println("CANcoder Pos vs expected: " + cancoderPos + " vs " + CANCODER_POSITION); - assertEquals(talonPos.getValue(), CANCODER_POSITION, SET_DELTA); - assertEquals(cancoderPos.getValue(), CANCODER_POSITION, SET_DELTA); + assertEquals(talonPos.getValueAsDouble(), CANCODER_POSITION, SET_DELTA); + assertEquals(cancoderPos.getValueAsDouble(), CANCODER_POSITION, SET_DELTA); } @Test @@ -164,8 +165,8 @@ public void testRemoteCANcoderPos() { /* Make sure Talon matches CANcoder, since it should be using CANcoder's position */ System.out.println("Talon Pos vs expected: " + talonPos + " vs " + CANCODER_POSITION); System.out.println("CANcoder Pos vs expected: " + cancoderPos + " vs " + CANCODER_POSITION); - assertEquals(talonPos.getValue(), CANCODER_POSITION, SET_DELTA); - assertEquals(cancoderPos.getValue(), CANCODER_POSITION, SET_DELTA); + assertEquals(talonPos.getValueAsDouble(), CANCODER_POSITION, SET_DELTA); + assertEquals(cancoderPos.getValueAsDouble(), CANCODER_POSITION, SET_DELTA); } private void retryConfigApply(Supplier toApply) { @@ -174,6 +175,6 @@ private void retryConfigApply(Supplier toApply) { do { finalCode = toApply.get(); } while (!finalCode.isOK() && --triesLeftOver > 0); - assert (finalCode.isOK()); + assertTrue(finalCode.isOK(), "Config apply failed: " + finalCode); } } diff --git a/src/test/LatencyCompensationTests.java b/src/test/LatencyCompensationTests.java index 24f748bb..177ae60a 100644 --- a/src/test/LatencyCompensationTests.java +++ b/src/test/LatencyCompensationTests.java @@ -19,6 +19,7 @@ import com.ctre.phoenix6.hardware.CANcoder; import com.ctre.phoenix6.hardware.TalonFX; import edu.wpi.first.hal.HAL; +import edu.wpi.first.wpilibj.Timer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -67,21 +68,20 @@ public void testLatencyCompensator() { assertTrue(status.isOK()); /* Wait a bit longer for the latency to actually do some work */ - try { - Thread.sleep(10); - } catch (Exception ex) { - } + Timer.delay(0.010); /* Calculate how much latency we'd expect */ double talonLatency = talonPos.getTimestamp().getLatency(); - double compensatedTalonPos = position + (velocity * talonLatency); + double compensatedTalonPos = + talonPos.getValueAsDouble() + (talonVel.getValueAsDouble() * talonLatency); double cancoderLatency = cancoderPos.getTimestamp().getLatency(); - double compensatedCANcoderPos = position + (velocity * cancoderLatency); + double compensatedCANcoderPos = + cancoderPos.getValueAsDouble() + (cancoderVel.getValueAsDouble() * cancoderLatency); /* Calculate compensated values before the assert to avoid timing issue related to it */ double functionCompensatedTalon = - BaseStatusSignal.getLatencyCompensatedValue(talonPos, talonVel); + BaseStatusSignal.getLatencyCompensatedValueAsDouble(talonPos, talonVel); double functionCompensatedCANcoder = - BaseStatusSignal.getLatencyCompensatedValue(cancoderPos, cancoderVel); + BaseStatusSignal.getLatencyCompensatedValueAsDouble(cancoderPos, cancoderVel); /* Assert the two methods match */ System.out.println("Talon Pos: " + compensatedTalonPos + " - " + functionCompensatedTalon); diff --git a/src/test/frc/robot/RobotConstantsTest.java b/src/test/frc/robot/RobotConstantsTest.java new file mode 100644 index 00000000..352eae37 --- /dev/null +++ b/src/test/frc/robot/RobotConstantsTest.java @@ -0,0 +1,119 @@ +package frc.robot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import frc.robot.subsystems.drive.SwerveConstants; +import org.junit.jupiter.api.Test; + +class RobotConstantsTest { + @Test + void robotDeviceCanBusesMatchSwerveConstants() { + assertEquals(SwerveConstants.kFLDriveCanbus, Constants.RobotDevices.FL_DRIVE.getBus()); + assertEquals(SwerveConstants.kFLSteerCanbus, Constants.RobotDevices.FL_ROTATION.getBus()); + assertEquals(SwerveConstants.kFLEncoderCanbus, Constants.RobotDevices.FL_CANCODER.getBus()); + + assertEquals(SwerveConstants.kFRDriveCanbus, Constants.RobotDevices.FR_DRIVE.getBus()); + assertEquals(SwerveConstants.kFRSteerCanbus, Constants.RobotDevices.FR_ROTATION.getBus()); + assertEquals(SwerveConstants.kFREncoderCanbus, Constants.RobotDevices.FR_CANCODER.getBus()); + + assertEquals(SwerveConstants.kBLDriveCanbus, Constants.RobotDevices.BL_DRIVE.getBus()); + assertEquals(SwerveConstants.kBLSteerCanbus, Constants.RobotDevices.BL_ROTATION.getBus()); + assertEquals(SwerveConstants.kBLEncoderCanbus, Constants.RobotDevices.BL_CANCODER.getBus()); + + assertEquals(SwerveConstants.kBRDriveCanbus, Constants.RobotDevices.BR_DRIVE.getBus()); + assertEquals(SwerveConstants.kBRSteerCanbus, Constants.RobotDevices.BR_ROTATION.getBus()); + assertEquals(SwerveConstants.kBREncoderCanbus, Constants.RobotDevices.BR_CANCODER.getBus()); + } + + @Test + void fieldLayoutConstantsAreInternallyConsistent() { + assertEquals(FieldConstants.defaultAprilTagType.getLayout(), FieldConstants.aprilTagLayout); + assertEquals( + FieldConstants.defaultAprilTagType.getLayout().getFieldLength(), + FieldConstants.fieldLength, + 1e-9); + assertEquals( + FieldConstants.defaultAprilTagType.getLayout().getFieldWidth(), + FieldConstants.fieldWidth, + 1e-9); + assertEquals( + FieldConstants.defaultAprilTagType.getLayout().getTags().size(), + FieldConstants.aprilTagCount); + assertNotNull(FieldConstants.defaultAprilTagType.getLayoutString()); + } + + @Test + void selectedTunableConstantsAreInUsableRanges() { + assertPositive(Constants.OperatorConstants.kRobotRelativeNudgeSpeedMetersPerSec); + assertPositive(Constants.SensorConstants.kRioAccelerometerSampleRateHz); + + assertPositive(Constants.DrivebaseConstants.kSysIdPreRunStopSecs); + assertPositive(Constants.DrivebaseConstants.kFeedforwardCharacterizationStartDelaySecs); + assertPositive(Constants.DrivebaseConstants.kFeedforwardCharacterizationRampRateVoltsPerSec); + assertPositive(Constants.DrivebaseConstants.kWheelRadiusCharacterizationStartDelaySecs); + assertPositive(Constants.DrivebaseConstants.kWheelRadiusCharacterizationMaxVelocityRadPerSec); + assertPositive(Constants.DrivebaseConstants.kWheelRadiusCharacterizationRampRateRadPerSecSq); + assertPositive(Constants.DrivebaseConstants.kDisabledCoastMinSeconds); + assertPositive(Constants.DrivebaseConstants.kDisabledVisionCoastBlendAlpha); + + assertPositive(Constants.FlywheelConstants.kMaxVoltage); + assertPositive(Constants.FlywheelConstants.kMotionMagicAccelerationRotPerSecSq); + assertPositive(Constants.FlywheelConstants.kMotionMagicJerkRotPerSecCubed); + assertPositive(Constants.FlywheelConstants.kSimGearing); + assertPositive(Constants.FlywheelConstants.kSimMomentOfInertiaKgMetersSq); + + assertEquals( + Math.min( + Constants.DrivebaseConstants.kDisabledVisionBlendAlpha, + Constants.DrivebaseConstants.kDisabledVisionCoastBlendAlpha), + Constants.DrivebaseConstants.kDisabledVisionCoastBlendAlpha, + 1e-9); + } + + @Test + void constantsDoNotExposeLegacyAliasNames() { + assertMissing(Constants.class, "loopPeriodSecs"); + assertMissing(Constants.class, "tuningMode"); + assertMissing(Constants.class, "G_TO_MPS2"); + + assertMissing(Constants.RobotConstants.class, "kRobotMass"); + assertMissing(Constants.RobotConstants.class, "kRobotMOI"); + assertMissing(Constants.RobotConstants.class, "kWheelCOF"); + assertMissing(Constants.RobotConstants.class, "kMaxWheelTorque"); + + assertMissing(Constants.PowerConstants.class, "kPDMType"); + assertMissing(Constants.PowerConstants.class, "kPDMCANid"); + assertMissing(Constants.PowerConstants.class, "kTotalMaxCurrent"); + assertMissing(Constants.PowerConstants.class, "kMotorPortMaxCurrent"); + assertMissing(Constants.PowerConstants.class, "kVoltageWarning"); + + assertMissing(Constants.DrivebaseConstants.class, "kMaxLinearSpeed"); + assertMissing(Constants.DrivebaseConstants.class, "kPStrafe"); + assertMissing(Constants.DrivebaseConstants.class, "kPSPin"); + assertMissing(Constants.DrivebaseConstants.class, "kWheelLockTime"); + assertMissing(Constants.DrivebaseConstants.class, "kHistorySize"); + + assertMissing(Constants.FlywheelConstants.class, "kFlywheelGearRatio"); + assertMissing(Constants.FlywheelConstants.class, "kSreal"); + assertMissing(Constants.FlywheelConstants.class, "kPsim"); + + assertMissing(Constants.VisionConstants.class, "maxAmbiguity"); + assertMissing(Constants.VisionConstants.class, "linearStdDevBaseline"); + + assertMissing(Constants.DeployConstants.class, "yagslDir"); + + assertMissing(Constants.OperatorConstants.class, "kDeadband"); + assertMissing(Constants.OperatorConstants.class, "kTurnConstant"); + assertMissing(Constants.OperatorConstants.class, "kJoystickSlewLimit"); + } + + private static void assertMissing(Class constantsClass, String fieldName) { + assertThrows(NoSuchFieldException.class, () -> constantsClass.getDeclaredField(fieldName)); + } + + private static void assertPositive(double value) { + org.junit.jupiter.api.Assertions.assertTrue(value > 0.0); + } +} diff --git a/src/test/RobotContainerTest.java b/src/test/frc/robot/RobotContainerTest.java similarity index 80% rename from src/test/RobotContainerTest.java rename to src/test/frc/robot/RobotContainerTest.java index 23d042a1..c400666c 100644 --- a/src/test/RobotContainerTest.java +++ b/src/test/frc/robot/RobotContainerTest.java @@ -7,7 +7,7 @@ // license that can be found in the LICENSE file at // the root directory of this project. -package org.littletonrobotics.frc2024; +package frc.robot; import static org.junit.jupiter.api.Assertions.fail; @@ -21,8 +21,7 @@ public void createRobotContainer() { try { new RobotContainer(); } catch (Exception e) { - e.printStackTrace(); - fail("Failed to instantiate RobotContainer, see stack trace above."); + fail("Failed to instantiate RobotContainer.", e); } } } diff --git a/src/test/frc/robot/commands/DriveCommandsTests.java b/src/test/frc/robot/commands/DriveCommandsTests.java new file mode 100644 index 00000000..1a82ac45 --- /dev/null +++ b/src/test/frc/robot/commands/DriveCommandsTests.java @@ -0,0 +1,63 @@ +package frc.robot.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import edu.wpi.first.hal.HAL; +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.wpilibj.simulation.DriverStationSim; +import edu.wpi.first.wpilibj2.command.Command; +import frc.robot.Constants.OperatorConstants; +import frc.robot.subsystems.drive.Drive; +import frc.robot.subsystems.imu.Imu; +import frc.robot.subsystems.imu.ImuIOSim; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DriveCommandsTests { + private static final double EPSILON = 1e-9; + + @BeforeEach + void setupHal() { + HAL.initialize(500, 0); + DriverStationSim.setEnabled(false); + DriverStationSim.notifyNewData(); + } + + @Test + void linearVelocityAppliesDeadbandAndSquaresMagnitudeWithoutChangingDirection() { + assertEquals(Translation2d.kZero, DriveCommands.getLinearVelocity(0.01, 0.01)); + + Translation2d velocity = DriveCommands.getLinearVelocity(0.6, 0.8); + assertEquals(1.0, velocity.getAngle().getCos() / 0.6, 1e-6); + assertEquals(1.0, velocity.getAngle().getSin() / 0.8, 1e-6); + assertEquals(1.0, velocity.getNorm(), EPSILON); + } + + @Test + void omegaAppliesDeadbandAndPreservesSignWhenSquared() { + double scaled = MathUtil.applyDeadband(0.5, OperatorConstants.kJoystickDeadband); + + assertEquals(0.0, DriveCommands.getOmega(0.01), EPSILON); + assertEquals(scaled * scaled, DriveCommands.getOmega(0.5), EPSILON); + assertEquals(-(scaled * scaled), DriveCommands.getOmega(-0.5), EPSILON); + } + + @Test + void utilityCommandFactoriesReturnCommands() { + Drive drive = new Drive(new Imu(new ImuIOSim())); + + Command stop = DriveCommands.stop(drive); + Command stopWithX = DriveCommands.stopWithX(drive); + Command brake = DriveCommands.setBrakeMode(drive, true); + Command zero = DriveCommands.zeroHeadingForAlliance(drive); + Command nudge = DriveCommands.robotRelativeNudge(drive, 0.1, 0.0, 0.0); + + assertNotNull(stop); + assertNotNull(stopWithX); + assertNotNull(brake); + assertNotNull(zero); + assertNotNull(nudge); + } +} diff --git a/src/test/frc/robot/generated/GeneratedTunerConstantsTest.java b/src/test/frc/robot/generated/GeneratedTunerConstantsTest.java new file mode 100644 index 00000000..cda13bb4 --- /dev/null +++ b/src/test/frc/robot/generated/GeneratedTunerConstantsTest.java @@ -0,0 +1,78 @@ +package frc.robot.generated; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; +import edu.wpi.first.math.util.Units; +import org.junit.jupiter.api.Test; + +class GeneratedTunerConstantsTest { + private static final double EPSILON = 1e-9; + private static final double TEMPLATE_XY_METERS = Units.inchesToMeters(10.0); + private static final double TEMPLATE_DRIVE_GEAR_RATIO = 6.746031746031747; + private static final double TEMPLATE_STEER_GEAR_RATIO = 21.428571428571427; + private static final double TEMPLATE_COUPLING_RATIO = 3.5714285714285716; + private static final double TEMPLATE_WHEEL_RADIUS_METERS = Units.inchesToMeters(2.0); + + @Test + void templateTunerConstantsUseZeroOffsetsAndTenInchModuleLocations() { + assertTemplateGeometry( + COMPBOTTunerConstants.FrontLeft, + COMPBOTTunerConstants.FrontRight, + COMPBOTTunerConstants.BackLeft, + COMPBOTTunerConstants.BackRight); + assertTemplateGeometry( + DEVBOT1TunerConstants.FrontLeft, + DEVBOT1TunerConstants.FrontRight, + DEVBOT1TunerConstants.BackLeft, + DEVBOT1TunerConstants.BackRight); + assertTemplateGeometry( + DEVBOT2TunerConstants.FrontLeft, + DEVBOT2TunerConstants.FrontRight, + DEVBOT2TunerConstants.BackLeft, + DEVBOT2TunerConstants.BackRight); + } + + @Test + void templateTunerConstantsUseMatchingRatios() { + assertTemplateRatios(COMPBOTTunerConstants.FrontLeft); + assertTemplateRatios(DEVBOT1TunerConstants.FrontLeft); + assertTemplateRatios(DEVBOT2TunerConstants.FrontLeft); + } + + private static void assertTemplateGeometry( + SwerveModuleConstants + frontLeft, + SwerveModuleConstants + frontRight, + SwerveModuleConstants + backLeft, + SwerveModuleConstants + backRight) { + assertModule(frontLeft, TEMPLATE_XY_METERS, TEMPLATE_XY_METERS); + assertModule(frontRight, TEMPLATE_XY_METERS, -TEMPLATE_XY_METERS); + assertModule(backLeft, -TEMPLATE_XY_METERS, TEMPLATE_XY_METERS); + assertModule(backRight, -TEMPLATE_XY_METERS, -TEMPLATE_XY_METERS); + } + + private static void assertModule( + SwerveModuleConstants + module, + double expectedX, + double expectedY) { + assertEquals(0.0, module.EncoderOffset, EPSILON); + assertEquals(expectedX, module.LocationX, EPSILON); + assertEquals(expectedY, module.LocationY, EPSILON); + } + + private static void assertTemplateRatios( + SwerveModuleConstants + module) { + assertEquals(TEMPLATE_DRIVE_GEAR_RATIO, module.DriveMotorGearRatio, EPSILON); + assertEquals(TEMPLATE_STEER_GEAR_RATIO, module.SteerMotorGearRatio, EPSILON); + assertEquals(TEMPLATE_COUPLING_RATIO, module.CouplingGearRatio, EPSILON); + assertEquals(TEMPLATE_WHEEL_RADIUS_METERS, module.WheelRadius, EPSILON); + } +} diff --git a/src/test/frc/robot/subsystems/accelerometer/RioAccelIOTests.java b/src/test/frc/robot/subsystems/accelerometer/RioAccelIOTests.java new file mode 100644 index 00000000..89d69cb0 --- /dev/null +++ b/src/test/frc/robot/subsystems/accelerometer/RioAccelIOTests.java @@ -0,0 +1,21 @@ +package frc.robot.subsystems.accelerometer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +class RioAccelIOTests { + @Test + void noopReportsDisconnectedZeroAcceleration() { + RioAccelIO.Inputs inputs = new RioAccelIO.Inputs(); + + RioAccelIO.noop().updateInputs(inputs); + + assertFalse(inputs.connected); + assertEquals(0L, inputs.timestampNs); + assertEquals(0.0, inputs.xG); + assertEquals(0.0, inputs.yG); + assertEquals(0.0, inputs.zG); + } +} diff --git a/src/test/frc/robot/subsystems/drive/DriveSubsystemTests.java b/src/test/frc/robot/subsystems/drive/DriveSubsystemTests.java new file mode 100644 index 00000000..fc454264 --- /dev/null +++ b/src/test/frc/robot/subsystems/drive/DriveSubsystemTests.java @@ -0,0 +1,101 @@ +package frc.robot.subsystems.drive; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.hal.HAL; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.wpilibj.simulation.DriverStationSim; +import frc.robot.subsystems.imu.Imu; +import frc.robot.subsystems.imu.ImuIOSim; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DriveSubsystemTests { + private static final double EPSILON = 1e-9; + + @BeforeEach + void setupHal() { + assertTrue(HAL.initialize(500, 0)); + DriverStationSim.setEnabled(false); + DriverStationSim.notifyNewData(); + } + + @Test + void yawRateUsesWrappedAngleDifference() { + double previous = Math.toRadians(179.0); + double current = Math.toRadians(-179.0); + + assertEquals(Math.toRadians(2.0) / 0.02, Drive.yawRateRadPerSec(previous, current, 0.02), 1e-6); + assertEquals(0.0, Drive.yawRateRadPerSec(previous, current, 0.0), EPSILON); + } + + @Test + void poseBufferAccessorsAreSafeWhenEmptyAndInterpolateWhenPopulated() { + Drive drive = new Drive(new Imu(new ImuIOSim())); + + assertTrue(Double.isNaN(drive.getPoseBufferOldestTime())); + assertTrue(Double.isNaN(drive.getPoseBufferNewestTime())); + assertTrue(drive.getPoseAtTime(1.0).isEmpty()); + + drive.poseBufferAddSample(1.0, new Pose2d(1.0, 0.0, Rotation2d.kZero)); + drive.poseBufferAddSample(2.0, new Pose2d(3.0, 0.0, Rotation2d.kZero)); + + assertEquals(1.0, drive.getPoseBufferOldestTime(), EPSILON); + assertEquals(2.0, drive.getPoseBufferNewestTime(), EPSILON); + assertEquals(2.0, drive.getPoseAtTime(1.5).orElseThrow().getX(), EPSILON); + } + + @Test + void modulePeriodicUsesCommonOdometryPrefix() { + ModuleIO fakeIo = + new ModuleIO() { + @Override + public void updateInputs(ModuleIOInputs inputs) { + inputs.driveConnected = true; + inputs.turnConnected = true; + inputs.turnEncoderConnected = true; + inputs.odometryTimestamps = new double[] {1.0, 2.0, 3.0}; + inputs.odometryDrivePositionsRad = new double[] {4.0, 5.0}; + inputs.odometryTurnPositions = + new Rotation2d[] {Rotation2d.kZero, Rotation2d.kCCW_Pi_2, Rotation2d.kPi}; + } + }; + Module module = new Module(fakeIo, 0); + + assertDoesNotThrow(module::periodic); + assertEquals(2, module.getOdometryPositions().length); + } + + @Test + void disabledCoastDoesNotCountBaselineSampleAsStationary() { + Drive drive = new Drive(new Imu(new ImuIOSim())); + SwerveModulePosition[] stationaryPositions = + new SwerveModulePosition[] { + new SwerveModulePosition(1.0, Rotation2d.kZero), + new SwerveModulePosition(1.0, Rotation2d.kZero), + new SwerveModulePosition(1.0, Rotation2d.kZero), + new SwerveModulePosition(1.0, Rotation2d.kZero) + }; + + DriverStationSim.setEnabled(true); + DriverStationSim.notifyNewData(); + drive.updateDisabledCoastState(true, false, 1.0, 0.0, stationaryPositions); + + DriverStationSim.setEnabled(false); + DriverStationSim.notifyNewData(); + drive.updateDisabledCoastState(false, true, 1.02, 0.0, stationaryPositions); + for (int i = 0; i < 9; i++) { + drive.updateDisabledCoastState(false, true, 1.30 + i * 0.02, 0.0, stationaryPositions); + } + + assertTrue(drive.isDisabledCoast(1.48)); + + drive.updateDisabledCoastState(false, true, 1.50, 0.0, stationaryPositions); + assertFalse(drive.isDisabledCoast(1.50)); + } +} diff --git a/src/test/frc/robot/subsystems/flywheel_example/FlywheelSysIdTests.java b/src/test/frc/robot/subsystems/flywheel_example/FlywheelSysIdTests.java new file mode 100644 index 00000000..7d5aac3f --- /dev/null +++ b/src/test/frc/robot/subsystems/flywheel_example/FlywheelSysIdTests.java @@ -0,0 +1,55 @@ +package frc.robot.subsystems.flywheel_example; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import org.junit.jupiter.api.Test; + +class FlywheelSysIdTests { + + @Test + void sysIdRoutinesUseDistinctMechanismNames() { + Flywheel flywheel = new Flywheel(new RecordingFlywheelIO()); + + assertEquals( + "Flywheel SysId Voltage Forward Quasistatic", + flywheel.sysIdVoltageQuasistatic(SysIdRoutine.Direction.kForward).getName()); + assertEquals( + "Flywheel SysId Voltage Reverse Dynamic", + flywheel.sysIdVoltageDynamic(SysIdRoutine.Direction.kReverse).getName()); + assertEquals( + "Flywheel SysId Duty Cycle Forward Quasistatic", + flywheel.sysIdDutyCycleQuasistatic(SysIdRoutine.Direction.kForward).getName()); + assertEquals( + "Flywheel SysId Duty Cycle Reverse Dynamic", + flywheel.sysIdDutyCycleDynamic(SysIdRoutine.Direction.kReverse).getName()); + } + + @Test + void velocityCommandsConvertRpmToMechanismRadiansPerSecond() { + RecordingFlywheelIO io = new RecordingFlywheelIO(); + Flywheel flywheel = new Flywheel(io); + + flywheel.runVelocity(6000.0); + assertEquals(Units.rotationsPerMinuteToRadiansPerSecond(6000.0), io.velocityRadPerSec); + + flywheel.runVelocityProfiled(3000.0); + assertEquals(Units.rotationsPerMinuteToRadiansPerSecond(3000.0), io.profiledVelocityRadPerSec); + } + + private static class RecordingFlywheelIO implements FlywheelIO { + double velocityRadPerSec; + double profiledVelocityRadPerSec; + + @Override + public void setVelocity(double velocityRadPerSec) { + this.velocityRadPerSec = velocityRadPerSec; + } + + @Override + public void setVelocityProfiled(double velocityRadPerSec) { + this.profiledVelocityRadPerSec = velocityRadPerSec; + } + } +} diff --git a/src/test/frc/robot/subsystems/imu/ImuIOSimTests.java b/src/test/frc/robot/subsystems/imu/ImuIOSimTests.java new file mode 100644 index 00000000..69e925fb --- /dev/null +++ b/src/test/frc/robot/subsystems/imu/ImuIOSimTests.java @@ -0,0 +1,45 @@ +package frc.robot.subsystems.imu; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ImuIOSimTests { + private static final double EPSILON = 1e-9; + + @Test + void odometrySamplesDrainOncePerUpdate() { + ImuIOSim io = new ImuIOSim(); + ImuIO.ImuIOInputs inputs = new ImuIO.ImuIOInputs(); + + io.simulationSetYawRad(1.0); + io.updateInputs(inputs); + assertEquals(1, inputs.odometryYawTimestamps.length); + assertEquals(1, inputs.odometryYawPositionsRad.length); + assertEquals(1.0, inputs.odometryYawPositionsRad[0], EPSILON); + + io.simulationSetYawRad(2.0); + io.updateInputs(inputs); + assertEquals(1, inputs.odometryYawTimestamps.length); + assertEquals(1, inputs.odometryYawPositionsRad.length); + assertEquals(2.0, inputs.odometryYawPositionsRad[0], EPSILON); + } + + @Test + void zeroYawClearsOldOdometrySamplesBeforeNextUpdate() { + ImuIOSim io = new ImuIOSim(); + ImuIO.ImuIOInputs inputs = new ImuIO.ImuIOInputs(); + + io.simulationSetYawRad(1.0); + io.updateInputs(inputs); + + io.zeroYawRad(0.25); + io.updateInputs(inputs); + + assertTrue(inputs.connected); + assertEquals(0.25, inputs.yawPositionRad, EPSILON); + assertEquals(1, inputs.odometryYawPositionsRad.length); + assertEquals(0.25, inputs.odometryYawPositionsRad[0], EPSILON); + } +} diff --git a/src/test/frc/robot/subsystems/vision/VisionIOTests.java b/src/test/frc/robot/subsystems/vision/VisionIOTests.java new file mode 100644 index 00000000..9238f0b3 --- /dev/null +++ b/src/test/frc/robot/subsystems/vision/VisionIOTests.java @@ -0,0 +1,77 @@ +package frc.robot.subsystems.vision; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.util.Units; +import org.junit.jupiter.api.Test; + +class VisionIOTests { + private static final double EPSILON = 1e-9; + + @Test + void limelightPoseParserUsesMetersAndDegrees() { + double[] sample = {1.2, 2.3, 0.4, 10.0, -20.0, 90.0, 35.0, 2.0, 1.5, 3.0, 0.2}; + + Pose3d pose = VisionIOLimelight.parsePose(sample); + + assertEquals(1.2, pose.getX(), EPSILON); + assertEquals(2.3, pose.getY(), EPSILON); + assertEquals(0.4, pose.getZ(), EPSILON); + assertEquals( + new Rotation3d( + Units.degreesToRadians(10.0), + Units.degreesToRadians(-20.0), + Units.degreesToRadians(90.0)), + pose.getRotation()); + } + + @Test + void limelightPoseParserRejectsShortArrays() { + assertThrows(IllegalArgumentException.class, () -> VisionIOLimelight.parsePose(new double[6])); + } + + @Test + void limelightSampleValidationRejectsMalformedSamples() { + assertFalse(VisionIOLimelight.isValidBotPoseSample(new double[0])); + + double[] negativeTagCount = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, -1.0, 0.0, 1.0, 0.0}; + assertFalse(VisionIOLimelight.isValidBotPoseSample(negativeTagCount)); + + double[] valid = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 1.0, 0.0}; + assertTrue(VisionIOLimelight.isValidBotPoseSample(valid)); + } + + @Test + void limelightUsedTagExtractionTrimsMissingPerTagData() { + double[] full = { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 2.0, 0.0, 1.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 12.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 + }; + assertArrayEquals(new int[] {7, 12}, VisionIOLimelight.extractUsedTagIds(full)); + + double[] truncated = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 2.0, 0.0, 1.0, 0.0, 7.0}; + assertArrayEquals(new int[] {7}, VisionIOLimelight.extractUsedTagIds(truncated)); + } + + @Test + void limelightTimestampSubtractsTotalLatencyMilliseconds() { + double[] sample = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 35.0, 1.0, 0.0, 1.0, 0.0}; + + assertEquals(9.965, VisionIOLimelight.timestampSeconds(10_000_000L, sample), EPSILON); + } + + @Test + void poseObservationCoalescesNullUsedTagsToEmptyArray() { + VisionIO.PoseObservation obs = + new VisionIO.PoseObservation( + 1.0, new Pose3d(), 0.0, 1, 2.0, VisionIO.PoseObservationType.PHOTONVISION, null); + + assertArrayEquals(new int[0], obs.usedTagIds()); + } +} diff --git a/src/test/frc/robot/util/UtilTests.java b/src/test/frc/robot/util/UtilTests.java new file mode 100644 index 00000000..94c759f2 --- /dev/null +++ b/src/test/frc/robot/util/UtilTests.java @@ -0,0 +1,79 @@ +package frc.robot.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.hal.HAL; +import edu.wpi.first.wpilibj.RobotController; +import java.util.NavigableMap; +import org.junit.jupiter.api.Test; + +class UtilTests { + private static final double EPSILON = 1e-9; + + @Test + void timeUtilReturnsSecondsFromAdvantageKitMicrosecondTimestamp() { + assertTrue(HAL.initialize(500, 0)); + + double expectedSeconds = RobotController.getFPGATime() * 1.0e-6; + double actualSeconds = TimeUtil.now(); + + assertEquals(expectedSeconds, actualSeconds, 0.050); + } + + @Test + void robotDeviceIdUsesValueEqualityAndHandlesMissingPowerPort() { + RobotDeviceId first = new RobotDeviceId(7, "rio", null); + RobotDeviceId second = new RobotDeviceId(7, new String("rio"), null); + RobotDeviceId differentBus = new RobotDeviceId(7, "DriveTrain", null); + + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + assertFalse(first.equals(differentBus)); + assertFalse(first.hasPowerPort()); + assertThrows(IllegalStateException.class, first::getPowerPort); + } + + @Test + void robotDeviceIdLooksUpCanBusThroughRegistry() { + RBSICANBusRegistry.initSim("rio"); + RobotDeviceId device = new RobotDeviceId(1, "rio", 0); + + assertThrows(IllegalStateException.class, device::getCANBus); + } + + @Test + void concurrentTimeInterpolatableBufferInterpolatesAndExposesReadOnlyRanges() { + ConcurrentTimeInterpolatableBuffer buffer = + ConcurrentTimeInterpolatableBuffer.createDoubleBuffer(1.0); + + assertTrue(buffer.getSample(0.0).isEmpty()); + assertTrue(buffer.getLatest().isEmpty()); + + buffer.addSample(1.0, 10.0); + buffer.addSample(2.0, 20.0); + + assertEquals(15.0, buffer.getSample(1.5).orElseThrow(), EPSILON); + assertEquals(10.0, buffer.getSample(0.5).orElseThrow(), EPSILON); + assertEquals(20.0, buffer.getSample(2.5).orElseThrow(), EPSILON); + assertEquals(2.0, buffer.getLatest().orElseThrow().getKey(), EPSILON); + + NavigableMap range = buffer.getSamplesInRange(1.0, true, 2.0, false); + assertEquals(1, range.size()); + assertEquals(10.0, range.get(1.0), EPSILON); + assertThrows(UnsupportedOperationException.class, () -> range.put(1.5, 15.0)); + assertTrue(buffer.getSamplesInRange(2.0, true, 1.0, true).isEmpty()); + } + + @Test + void overrideSwitchesRejectInvalidIndexes() { + OverrideSwitches switches = new OverrideSwitches(5); + + assertThrows(IllegalArgumentException.class, () -> switches.getDriverSwitch(-1)); + assertThrows(IllegalArgumentException.class, () -> switches.getDriverSwitch(3)); + assertThrows(IllegalArgumentException.class, () -> switches.getOperatorSwitch(-1)); + assertThrows(IllegalArgumentException.class, () -> switches.getOperatorSwitch(5)); + } +} diff --git a/src/test/frc/robot/util/VirtualSubsystemTests.java b/src/test/frc/robot/util/VirtualSubsystemTests.java new file mode 100644 index 00000000..2d63f2cc --- /dev/null +++ b/src/test/frc/robot/util/VirtualSubsystemTests.java @@ -0,0 +1,75 @@ +package frc.robot.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VirtualSubsystemTests { + + @BeforeEach + void resetRegistry() { + VirtualSubsystem.resetForTesting(); + } + + @AfterEach + void cleanupRegistry() { + VirtualSubsystem.resetForTesting(); + } + + @Test + void periodicAllOrdersByPriorityThenConstructionOrder() { + List calls = new ArrayList<>(); + + new RecordingSubsystem("middleA", 0, calls); + new RecordingSubsystem("early", -30, calls); + new RecordingSubsystem("middleB", 0, calls); + new RecordingSubsystem("late", 20, calls); + + VirtualSubsystem.periodicAll(); + + assertEquals(List.of("early", "middleA", "middleB", "late"), calls); + } + + @Test + void periodicAllResortsWhenSubsystemIsAddedAfterFirstRun() { + List calls = new ArrayList<>(); + + new RecordingSubsystem("middleA", 0, calls); + new RecordingSubsystem("middleB", 0, calls); + + VirtualSubsystem.periodicAll(); + calls.clear(); + + new RecordingSubsystem("early", -10, calls); + + VirtualSubsystem.periodicAll(); + + assertEquals(List.of("early", "middleA", "middleB"), calls); + } + + private static final class RecordingSubsystem extends VirtualSubsystem { + private final String name; + private final int priority; + private final List calls; + + RecordingSubsystem(String name, int priority, List calls) { + this.name = name; + this.priority = priority; + this.calls = calls; + } + + @Override + protected int getPeriodPriority() { + return priority; + } + + @Override + protected void rbsiPeriodic() { + calls.add(name); + } + } +}