From 80b84243f77c1c1aba47024f289b81d01de70be6 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Wed, 1 Oct 2025 17:34:36 -0400 Subject: [PATCH 01/93] updates --- simgui.json | 6 +- .../black_robot/akit_swerve_drivetrain.json | 3 + src/main/deploy/black_robot/cameras.json | 4 +- .../deploy/black_robot/cameras/intake.json | 13 ++++ .../black_robot/cameras/localization.json | 13 ++++ .../deploy/black_robot/cameras/quest.json | 11 ++++ .../deploy/black_robot/cameras/shooter.json | 16 +++++ .../deploy/black_robot/competition_mode.json | 6 ++ src/main/deploy/black_robot/controllers.json | 3 +- .../controllers/axis/driver_left_trigger.json | 8 +++ .../controllers/axis/driver_left_x.json | 8 +++ .../controllers/axis/driver_left_y.json | 8 +++ .../axis/driver_right_trigger.json | 8 +++ .../controllers/axis/driver_right_x.json | 8 +++ .../controllers/axis/operator_left_y.json | 9 +++ .../controllers/axis/operator_right_y.json | 9 +++ .../black_robot/controllers/driver.json | 11 ++++ .../black_robot/controllers/operator.json | 8 +++ src/main/deploy/black_robot/demo_mode.json | 6 ++ .../black_robot/drive_modules/backleft.json | 5 ++ .../black_robot/drive_modules/backright.json | 5 ++ .../black_robot/drive_modules/frontleft.json | 5 ++ .../black_robot/drive_modules/frontright.json | 5 ++ .../deploy/black_robot/field/game_pieces.json | 10 +++ src/main/deploy/black_robot/robot.json | 16 +++++ .../subsystems/example/percent_motor.json | 11 ++++ .../subsystems/example/velocity_motor.json | 18 ++++++ .../subsystems/example/yams_arm.json | 64 +++++++++++++++++++ .../subsystems/example/yams_elevator.json | 59 +++++++++++++++++ .../subsystems/example/yams_pivot.json | 57 +++++++++++++++++ .../deploy/black_robot/yagsl_drivetrain.json | 24 +++++++ .../yagsl_swerve/controllerproperties.json | 8 +++ .../yagsl_swerve/modules/backleft.json | 26 ++++++++ .../yagsl_swerve/modules/backright.json | 26 ++++++++ .../yagsl_swerve/modules/frontleft.json | 26 ++++++++ .../yagsl_swerve/modules/frontright.json | 26 ++++++++ .../modules/physicalproperties.json | 24 +++++++ .../yagsl_swerve/modules/pidfproperties.json | 16 +++++ .../black_robot/yagsl_swerve/swervedrive.json | 14 ++++ src/main/deploy/robots.json | 2 +- src/main/java/frc/robot/BuildConstants.java | 12 ++-- 41 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 src/main/deploy/black_robot/akit_swerve_drivetrain.json create mode 100644 src/main/deploy/black_robot/cameras/intake.json create mode 100644 src/main/deploy/black_robot/cameras/localization.json create mode 100644 src/main/deploy/black_robot/cameras/quest.json create mode 100644 src/main/deploy/black_robot/cameras/shooter.json create mode 100644 src/main/deploy/black_robot/competition_mode.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_left_x.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_left_y.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_right_x.json create mode 100644 src/main/deploy/black_robot/controllers/axis/operator_left_y.json create mode 100644 src/main/deploy/black_robot/controllers/axis/operator_right_y.json create mode 100644 src/main/deploy/black_robot/controllers/driver.json create mode 100644 src/main/deploy/black_robot/controllers/operator.json create mode 100644 src/main/deploy/black_robot/demo_mode.json create mode 100644 src/main/deploy/black_robot/drive_modules/backleft.json create mode 100644 src/main/deploy/black_robot/drive_modules/backright.json create mode 100644 src/main/deploy/black_robot/drive_modules/frontleft.json create mode 100644 src/main/deploy/black_robot/drive_modules/frontright.json create mode 100644 src/main/deploy/black_robot/field/game_pieces.json create mode 100644 src/main/deploy/black_robot/robot.json create mode 100644 src/main/deploy/black_robot/subsystems/example/percent_motor.json create mode 100644 src/main/deploy/black_robot/subsystems/example/velocity_motor.json create mode 100644 src/main/deploy/black_robot/subsystems/example/yams_arm.json create mode 100644 src/main/deploy/black_robot/subsystems/example/yams_elevator.json create mode 100644 src/main/deploy/black_robot/subsystems/example/yams_pivot.json create mode 100644 src/main/deploy/black_robot/yagsl_drivetrain.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/backright.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/swervedrive.json diff --git a/simgui.json b/simgui.json index dbb3ee21..86f6fc3b 100644 --- a/simgui.json +++ b/simgui.json @@ -70,6 +70,7 @@ "/LiveWindow/Ungrouped/Pigeon 2 [13]": "Gyro", "/LiveWindow/Ungrouped/Scheduler": "Scheduler", "/Shuffleboard/BabySwerve/Auto Modes": "String Chooser", + "/Shuffleboard/BlackRobot/Auto Modes": "String Chooser", "/Shuffleboard/ExampleRobot/Auto Modes": "String Chooser", "/SmartDashboard/Alerts": "Alerts", "/SmartDashboard/Arm/mechanism": "Mechanism2d", @@ -667,7 +668,10 @@ "left": 534, "right": 3466, "top": 291, - "width": 17.54825210571289 + "width": 17.54825210571289, + "window": { + "visible": true + } }, "/SmartDashboard/Robot Visual": { "window": { diff --git a/src/main/deploy/black_robot/akit_swerve_drivetrain.json b/src/main/deploy/black_robot/akit_swerve_drivetrain.json new file mode 100644 index 00000000..42f49455 --- /dev/null +++ b/src/main/deploy/black_robot/akit_swerve_drivetrain.json @@ -0,0 +1,3 @@ +{ + "type": "SparkTalon" +} diff --git a/src/main/deploy/black_robot/cameras.json b/src/main/deploy/black_robot/cameras.json index 0059afc4..1d32743e 100644 --- a/src/main/deploy/black_robot/cameras.json +++ b/src/main/deploy/black_robot/cameras.json @@ -1,7 +1,9 @@ { "cameras": [ + "intake.json", "localization.json", - "shooter.json" + "shooter.json", + "quest.json" ], "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" diff --git a/src/main/deploy/black_robot/cameras/intake.json b/src/main/deploy/black_robot/cameras/intake.json new file mode 100644 index 00000000..33938add --- /dev/null +++ b/src/main/deploy/black_robot/cameras/intake.json @@ -0,0 +1,13 @@ +{ + "name": "intake", + "use": "target", + "type": "limelight", + "column": 0, + "x": 0.15, + "y": 0.05, + "z": 0.2, + "roll": 0, + "pitch": 10, + "yaw": 0, + "targetHeight": 0.02 +} diff --git a/src/main/deploy/black_robot/cameras/localization.json b/src/main/deploy/black_robot/cameras/localization.json new file mode 100644 index 00000000..706b561a --- /dev/null +++ b/src/main/deploy/black_robot/cameras/localization.json @@ -0,0 +1,13 @@ +{ + "name": "localization", + "use": "apriltag", + "type": "limelight", + "strategy": "LOWEST_AMBIGUITY", + "column": 0, + "x": -0.35, + "y": -0.25, + "z": 0.5, + "roll": 0, + "pitch": -20, + "yaw": 180 +} diff --git a/src/main/deploy/black_robot/cameras/quest.json b/src/main/deploy/black_robot/cameras/quest.json new file mode 100644 index 00000000..df412311 --- /dev/null +++ b/src/main/deploy/black_robot/cameras/quest.json @@ -0,0 +1,11 @@ +{ + "name": "quest", + "use": "quest", + "column": 0, + "x": 0, + "y": 0, + "z": 18, + "roll": 0, + "pitch": 0, + "yaw": 0 +} diff --git a/src/main/deploy/black_robot/cameras/shooter.json b/src/main/deploy/black_robot/cameras/shooter.json new file mode 100644 index 00000000..b60f68bc --- /dev/null +++ b/src/main/deploy/black_robot/cameras/shooter.json @@ -0,0 +1,16 @@ +{ + "name": "shooter", + "use": "target", + "type": "photonvision", + "column": 0, + "x": -0.25, + "y": 0, + "z": 0.25, + "roll": 0, + "pitch": 20, + "yaw": 180, + "targetFiducialIds": [ + 4, + 11 + ] +} diff --git a/src/main/deploy/black_robot/competition_mode.json b/src/main/deploy/black_robot/competition_mode.json new file mode 100644 index 00000000..f67b949b --- /dev/null +++ b/src/main/deploy/black_robot/competition_mode.json @@ -0,0 +1,6 @@ +{ + "maxSpeed": 6.0, + "maxAngularSpeed": 6.0, + "maxAccelleration": 3.0, + "maxAngularAccelleration": 18.75 +} diff --git a/src/main/deploy/black_robot/controllers.json b/src/main/deploy/black_robot/controllers.json index 901f9478..c6c86acb 100644 --- a/src/main/deploy/black_robot/controllers.json +++ b/src/main/deploy/black_robot/controllers.json @@ -1,5 +1,6 @@ { "controllers": [ - "driver.json" + "driver.json", + "operator.json" ] } diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json new file mode 100644 index 00000000..41b79133 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json @@ -0,0 +1,8 @@ +{ + "channel": 2, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_x.json b/src/main/deploy/black_robot/controllers/axis/driver_left_x.json new file mode 100644 index 00000000..d77f0e91 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_x.json @@ -0,0 +1,8 @@ +{ + "channel": 0, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_y.json b/src/main/deploy/black_robot/controllers/axis/driver_left_y.json new file mode 100644 index 00000000..d826731e --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_y.json @@ -0,0 +1,8 @@ +{ + "channel": 1, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json new file mode 100644 index 00000000..47b46917 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json @@ -0,0 +1,8 @@ +{ + "channel": 3, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_right_x.json b/src/main/deploy/black_robot/controllers/axis/driver_right_x.json new file mode 100644 index 00000000..8f981e23 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_right_x.json @@ -0,0 +1,8 @@ +{ + "channel": 4, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/operator_left_y.json b/src/main/deploy/black_robot/controllers/axis/operator_left_y.json new file mode 100644 index 00000000..c13bd0e0 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/operator_left_y.json @@ -0,0 +1,9 @@ +{ + "channel": 1, + "deadband": 0.07, + "invert": "true", + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0, + "rate": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/operator_right_y.json b/src/main/deploy/black_robot/controllers/axis/operator_right_y.json new file mode 100644 index 00000000..2668956f --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/operator_right_y.json @@ -0,0 +1,9 @@ +{ + "channel": 5, + "deadband": 0.07, + "invert": "true", + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0, + "rate": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/driver.json b/src/main/deploy/black_robot/controllers/driver.json new file mode 100644 index 00000000..e2dbc76a --- /dev/null +++ b/src/main/deploy/black_robot/controllers/driver.json @@ -0,0 +1,11 @@ +{ + "name": "driver", + "port": 0, + "axis": [ + "driver_left_y.json", + "driver_left_x.json", + "driver_right_x.json", + "driver_left_trigger.json", + "driver_right_trigger.json" + ] +} diff --git a/src/main/deploy/black_robot/controllers/operator.json b/src/main/deploy/black_robot/controllers/operator.json new file mode 100644 index 00000000..858cfea8 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/operator.json @@ -0,0 +1,8 @@ +{ + "name": "operator", + "port": 1, + "axis": [ + "operator_left_y.json", + "operator_right_y.json" + ] +} diff --git a/src/main/deploy/black_robot/demo_mode.json b/src/main/deploy/black_robot/demo_mode.json new file mode 100644 index 00000000..f60f73c9 --- /dev/null +++ b/src/main/deploy/black_robot/demo_mode.json @@ -0,0 +1,6 @@ +{ + "maxSpeed": 2.0, + "maxAngularSpeed": 2.0, + "maxAccelleration": 1.0, + "maxAngularAccelleration": 9.0 +} diff --git a/src/main/deploy/black_robot/drive_modules/backleft.json b/src/main/deploy/black_robot/drive_modules/backleft.json new file mode 100644 index 00000000..c62ce932 --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/backleft.json @@ -0,0 +1,5 @@ +{ + "s": 0.19542, + "v": 2.2924, + "a": 0.35934 +} diff --git a/src/main/deploy/black_robot/drive_modules/backright.json b/src/main/deploy/black_robot/drive_modules/backright.json new file mode 100644 index 00000000..aee7fd95 --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/backright.json @@ -0,0 +1,5 @@ +{ + "s": 0.18096, + "v": 2.2915, + "a": 0.37156 +} diff --git a/src/main/deploy/black_robot/drive_modules/frontleft.json b/src/main/deploy/black_robot/drive_modules/frontleft.json new file mode 100644 index 00000000..bdf7d3fa --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/frontleft.json @@ -0,0 +1,5 @@ +{ + "s": 0.21848, + "v": 2.3118, + "a": 0.20314 +} diff --git a/src/main/deploy/black_robot/drive_modules/frontright.json b/src/main/deploy/black_robot/drive_modules/frontright.json new file mode 100644 index 00000000..36a1c209 --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/frontright.json @@ -0,0 +1,5 @@ +{ + "s": 0.18434, + "v": 2.3018, + "a": 0.30992 +} diff --git a/src/main/deploy/black_robot/field/game_pieces.json b/src/main/deploy/black_robot/field/game_pieces.json new file mode 100644 index 00000000..b13b37c1 --- /dev/null +++ b/src/main/deploy/black_robot/field/game_pieces.json @@ -0,0 +1,10 @@ +{ + "gamePieces": [ + { + "x": 2, + "y": 7, + "type": "Coral", + "rotation": 0 + } + ] +} diff --git a/src/main/deploy/black_robot/robot.json b/src/main/deploy/black_robot/robot.json new file mode 100644 index 00000000..0ced2872 --- /dev/null +++ b/src/main/deploy/black_robot/robot.json @@ -0,0 +1,16 @@ +{ + "userConfig": "competition_mode.json", + "driveType": "YAGSL_SWERVE_DRIVE", + "trackWidth": 16.8, + "trackWidthUom": "in", + "wheelBase": 18.5, + "wheelBaseUom": "in", + "wheelDiameter": 0.103386904, + "wheelDiameterUom": "m", + "physicalMaxSpeed": 5.93, + "physicalMaxSpeedUom": "m/s", + "driveMotorGearRatio": 1.0, + "loadSimulatedField": false, + "gamePieceA": "Coral", + "gamePieceB": "Algae" +} diff --git a/src/main/deploy/black_robot/subsystems/example/percent_motor.json b/src/main/deploy/black_robot/subsystems/example/percent_motor.json new file mode 100644 index 00000000..027c231f --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/percent_motor.json @@ -0,0 +1,11 @@ +{ + "name": "percent_motor", + "controller": "spark", + "type": "Neo", + "id": 11, + "gearing": 1.0, + "momentOfInertiaKgMSq": 1.0, + "x": 0.5, + "y": 0.0, + "z": 0.25 +} diff --git a/src/main/deploy/black_robot/subsystems/example/velocity_motor.json b/src/main/deploy/black_robot/subsystems/example/velocity_motor.json new file mode 100644 index 00000000..185de2e0 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/velocity_motor.json @@ -0,0 +1,18 @@ +{ + "name": "velocity_motor", + "controller": "spark", + "type": "Neo", + "id": 14, + "gearing": 1.0, + "momentOfInertiaKgMSq": 1.0, + "x": -0.25, + "y": 0.0, + "z": 0.25, + "kP": 0.1, + "kI": 0.0, + "kD": 0.01, + "iZone": 0.0, + "kS": 0.1, + "kV": 0.1, + "kA": 0.0 +} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_arm.json b/src/main/deploy/black_robot/subsystems/example/yams_arm.json new file mode 100644 index 00000000..38cf9e27 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/yams_arm.json @@ -0,0 +1,64 @@ +{ + "motorSetup": { + "name": "Arm", + "controllerType": "talonfx", + "motorType": "KrakenX60", + "canId": 11 + }, + "motorSystemId": { + "feedBack": { + "p": 4, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + }, + "maxVelocity": { + "val": 180, + "uom": "deg/s" + }, + "maxAcceleration": { + "val": 90, + "uom": "deg/s^2" + } + }, + "length": { + "val": 0.135, + "uom": "in" + }, + "lowerHardLimit": { + "val": -80, + "uom": "deg" + }, + "upperHardLimit": { + "val": 200, + "uom": "deg" + }, + "startingAngle": { + "val": 0, + "uom": "deg" + }, + "lowerSoftLimit": { + "val": -30, + "uom": "deg" + }, + "upperSoftLimit": { + "val": 100, + "uom": "deg" + }, + "gearing": [ + 3, + 4 + ], + "mass": { + "val": 1, + "uom": "lbs" + }, + "horizontalZero": { + "val": 0, + "uom": "deg" + } +} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_elevator.json b/src/main/deploy/black_robot/subsystems/example/yams_elevator.json new file mode 100644 index 00000000..55046e05 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/yams_elevator.json @@ -0,0 +1,59 @@ +{ + "motorSetup": { + "name": "Elevator", + "controllerType": "spark", + "motorType": "Neo", + "canId": 10 + }, + "motorSystemId": { + "feedBack": { + "p": 2.7739E-10, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.013799, + "g": 0.24168, + "v": 10.219, + "a": 0.2153 + }, + "maxVelocity": { + "val": 4, + "uom": "m/s" + }, + "maxAcceleration": { + "val": 20, + "uom": "m/s^2" + }, + "controlMode": "OPEN_LOOP" + }, + "sprocketTeeth": 22, + "lowerSoftLimit": { + "val": 0.1, + "uom": "m" + }, + "upperSoftLimit": { + "val": 2, + "uom": "m" + }, + "lowerHardLimit": { + "val": 0.1, + "uom": "m" + }, + "upperHardLimit": { + "val": 3, + "uom": "m" + }, + "gearing": [ + 3, + 4 + ], + "startingPosition": { + "val": 0.5, + "uom": "m" + }, + "mass": { + "val": 16, + "uom": "lbs" + } +} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_pivot.json b/src/main/deploy/black_robot/subsystems/example/yams_pivot.json new file mode 100644 index 00000000..329453e1 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/yams_pivot.json @@ -0,0 +1,57 @@ +{ + "motorSetup": { + "name": "Turret", + "controllerType": "talonfx", + "motorType": "KrakenX60", + "canId": 12 + }, + "motorSystemId": { + "feedBack": { + "p": 4, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + }, + "maxVelocity": { + "val": 180, + "uom": "deg/s" + }, + "maxAcceleration": { + "val": 90, + "uom": "deg/s^2" + } + }, + "lowerHardLimit": { + "val": -100, + "uom": "deg" + }, + "upperHardLimit": { + "val": 200, + "uom": "deg" + }, + "startingAngle": { + "val": 0, + "uom": "deg" + }, + "lowerSoftLimit": { + "val": -30, + "uom": "deg" + }, + "upperSoftLimit": { + "val": 100, + "uom": "deg" + }, + "gearing": [ + 3, + 4 + ], + "startingPosition": { + "val": 0, + "uom": "deg" + }, + "moi": 0.001 +} diff --git a/src/main/deploy/black_robot/yagsl_drivetrain.json b/src/main/deploy/black_robot/yagsl_drivetrain.json new file mode 100644 index 00000000..c6beb5d7 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_drivetrain.json @@ -0,0 +1,24 @@ +{ + "directory": "basic_robot/yagsl_swerve", + "turningMotorGearRatio": 1.0, + "driveModules": [ + "frontleft.json", + "frontright.json", + "backleft.json", + "backright.json" + ], + "startingPose": { + "x": { + "val": 1.0, + "uom": "m" + }, + "y": { + "val": 1.0, + "uom": "m" + }, + "rotation": { + "val": 0.0, + "uom": "deg" + } + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json b/src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json new file mode 100644 index 00000000..dd353caf --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json @@ -0,0 +1,8 @@ +{ + "angleJoystickRadiusDeadband": 0.05, + "heading": { + "p": 0.4, + "i": 0, + "d": 0.01 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json new file mode 100644 index 00000000..2ac2a1ab --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 17, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 6, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": -8.37, + "left": 10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json new file mode 100644 index 00000000..eea317b8 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 18, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 2, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": -8.37, + "left": -10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json new file mode 100644 index 00000000..1c03c3f1 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 15, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 4, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": 8.37, + "left": 10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json new file mode 100644 index 00000000..6218565f --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 16, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 3, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": 8.37, + "left": -10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json new file mode 100644 index 00000000..c921b000 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json @@ -0,0 +1,24 @@ +{ + "conversionFactors": { + "angle": { + "gearRatio": 12.8, + "factor": 0 + }, + "drive": { + "gearRatio": 8.14, + "diameter": 4, + "factor": 0 + } + }, + "currentLimit": { + "drive": 100, + "angle": 20 + }, + "rampRate": { + "drive": 0.15, + "angle": 0.05 + }, + "wheelGripCoefficientOfFriction": 1.19, + "optimalVoltage": 12, + "robotMass": 75 +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json new file mode 100644 index 00000000..5924fdbb --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json @@ -0,0 +1,16 @@ +{ + "drive": { + "p": 0.00023, + "i": 0.0000002, + "d": 1, + "f": 0, + "iz": 0 + }, + "angle": { + "p": 0.0020645, + "i": 0, + "d": 0, + "f": 0.001, + "iz": 0 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json b/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json new file mode 100644 index 00000000..f4876233 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json @@ -0,0 +1,14 @@ +{ + "imu": { + "type": "pigeon2", + "id": 13, + "canbus": "" + }, + "invertedIMU": false, + "modules": [ + "frontleft.json", + "frontright.json", + "backleft.json", + "backright.json" + ] +} diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 64da13eb..40583cef 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -4,7 +4,7 @@ "basic_robot": { "id": "basic_robot", "robotClass": "frc.robot.ExampleRobot", - "simulate": true + "simulate": false }, "black_robot": { "id": "black_robot", diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 97530993..c1ca3a51 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 16; - public static final String GIT_SHA = "6d6bed62ee19993841c33ec8571bc7d3e827072a"; - public static final String GIT_DATE = "2025-10-01 17:02:59 EDT"; - public static final String GIT_BRANCH = "main"; - public static final String BUILD_DATE = "2025-10-01 17:04:35 EDT"; - public static final long BUILD_UNIX_TIME = 1759352675342L; + public static final int GIT_REVISION = 18; + public static final String GIT_SHA = "34af230763e749d8867c0f36a9b716c3945a1af8"; + public static final String GIT_DATE = "2025-10-01 17:06:27 EDT"; + public static final String GIT_BRANCH = "7-swerve-updates"; + public static final String BUILD_DATE = "2025-10-01 17:32:55 EDT"; + public static final long BUILD_UNIX_TIME = 1759354375311L; public static final int DIRTY = 1; private BuildConstants() {} From b7f64a13ba7f5b114331ca3b57a4c4d99f29faf7 Mon Sep 17 00:00:00 2001 From: Curt Date: Sun, 5 Oct 2025 15:10:09 -0400 Subject: [PATCH 02/93] Remove butterbup --- .vscode/settings.json | 3 ++- .../basic_robot/subsystems/example.json | 7 +++++ .../akit_swerve_drivetrain.json | 3 --- src/main/deploy/buttercup_swerve/cameras.json | 9 ------- .../buttercup_swerve/cameras/intake.json | 13 ---------- .../cameras/localization.json | 13 ---------- .../buttercup_swerve/cameras/quest.json | 11 -------- .../buttercup_swerve/cameras/shooter.json | 16 ------------ .../buttercup_swerve/competition_mode.json | 6 ----- .../deploy/buttercup_swerve/controllers.json | 6 ----- .../controllers/axis/driver_left_trigger.json | 8 ------ .../controllers/axis/driver_left_x.json | 8 ------ .../controllers/axis/driver_left_y.json | 8 ------ .../axis/driver_right_trigger.json | 8 ------ .../controllers/axis/driver_right_x.json | 8 ------ .../controllers/axis/operator_left_y.json | 9 ------- .../controllers/axis/operator_right_y.json | 9 ------- .../buttercup_swerve/controllers/driver.json | 11 -------- .../controllers/operator.json | 8 ------ .../deploy/buttercup_swerve/demo_mode.json | 6 ----- .../drive_modules/backleft.json | 5 ---- .../drive_modules/backright.json | 5 ---- .../drive_modules/frontleft.json | 5 ---- .../drive_modules/frontright.json | 5 ---- .../buttercup_swerve/field/game_pieces.json | 10 ------- src/main/deploy/buttercup_swerve/robot.json | 16 ------------ .../buttercup_swerve/yagsl_drivetrain.json | 24 ----------------- .../yagsl_swerve/controllerproperties.json | 8 ------ .../yagsl_swerve/modules/backleft.json | 26 ------------------- .../yagsl_swerve/modules/backright.json | 26 ------------------- .../yagsl_swerve/modules/frontleft.json | 26 ------------------- .../yagsl_swerve/modules/frontright.json | 26 ------------------- .../modules/physicalproperties.json | 24 ----------------- .../yagsl_swerve/modules/pidfproperties.json | 16 ------------ .../yagsl_swerve/swervedrive.json | 14 ---------- src/main/deploy/pathplanner/settings.json | 2 +- 36 files changed, 10 insertions(+), 398 deletions(-) create mode 100644 src/main/deploy/basic_robot/subsystems/example.json delete mode 100644 src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json delete mode 100644 src/main/deploy/buttercup_swerve/cameras.json delete mode 100644 src/main/deploy/buttercup_swerve/cameras/intake.json delete mode 100644 src/main/deploy/buttercup_swerve/cameras/localization.json delete mode 100644 src/main/deploy/buttercup_swerve/cameras/quest.json delete mode 100644 src/main/deploy/buttercup_swerve/cameras/shooter.json delete mode 100644 src/main/deploy/buttercup_swerve/competition_mode.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/driver.json delete mode 100644 src/main/deploy/buttercup_swerve/controllers/operator.json delete mode 100644 src/main/deploy/buttercup_swerve/demo_mode.json delete mode 100644 src/main/deploy/buttercup_swerve/drive_modules/backleft.json delete mode 100644 src/main/deploy/buttercup_swerve/drive_modules/backright.json delete mode 100644 src/main/deploy/buttercup_swerve/drive_modules/frontleft.json delete mode 100644 src/main/deploy/buttercup_swerve/drive_modules/frontright.json delete mode 100644 src/main/deploy/buttercup_swerve/field/game_pieces.json delete mode 100644 src/main/deploy/buttercup_swerve/robot.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_drivetrain.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json delete mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a9e0e63..3b81250d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,6 @@ null ], "java.test.defaultConfig": "WPIlibUnitTests", - "java.debug.settings.onBuildFailureProceed": true + "java.debug.settings.onBuildFailureProceed": true, + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable" } diff --git a/src/main/deploy/basic_robot/subsystems/example.json b/src/main/deploy/basic_robot/subsystems/example.json new file mode 100644 index 00000000..928061d8 --- /dev/null +++ b/src/main/deploy/basic_robot/subsystems/example.json @@ -0,0 +1,7 @@ +{ + "devices": { + "percent_motor": "example/percent_motor.json", + "velocity_motor": "example/velocity_motor.json" + }, + "display": true +} diff --git a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json deleted file mode 100644 index 42f49455..00000000 --- a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "SparkTalon" -} diff --git a/src/main/deploy/buttercup_swerve/cameras.json b/src/main/deploy/buttercup_swerve/cameras.json deleted file mode 100644 index 716ab962..00000000 --- a/src/main/deploy/buttercup_swerve/cameras.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "cameras": [ - - "localization.json", - "shooter.json" - ], - "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", - "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" -} diff --git a/src/main/deploy/buttercup_swerve/cameras/intake.json b/src/main/deploy/buttercup_swerve/cameras/intake.json deleted file mode 100644 index 33938add..00000000 --- a/src/main/deploy/buttercup_swerve/cameras/intake.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "intake", - "use": "target", - "type": "limelight", - "column": 0, - "x": 0.15, - "y": 0.05, - "z": 0.2, - "roll": 0, - "pitch": 10, - "yaw": 0, - "targetHeight": 0.02 -} diff --git a/src/main/deploy/buttercup_swerve/cameras/localization.json b/src/main/deploy/buttercup_swerve/cameras/localization.json deleted file mode 100644 index 706b561a..00000000 --- a/src/main/deploy/buttercup_swerve/cameras/localization.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "localization", - "use": "apriltag", - "type": "limelight", - "strategy": "LOWEST_AMBIGUITY", - "column": 0, - "x": -0.35, - "y": -0.25, - "z": 0.5, - "roll": 0, - "pitch": -20, - "yaw": 180 -} diff --git a/src/main/deploy/buttercup_swerve/cameras/quest.json b/src/main/deploy/buttercup_swerve/cameras/quest.json deleted file mode 100644 index df412311..00000000 --- a/src/main/deploy/buttercup_swerve/cameras/quest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "quest", - "use": "quest", - "column": 0, - "x": 0, - "y": 0, - "z": 18, - "roll": 0, - "pitch": 0, - "yaw": 0 -} diff --git a/src/main/deploy/buttercup_swerve/cameras/shooter.json b/src/main/deploy/buttercup_swerve/cameras/shooter.json deleted file mode 100644 index b60f68bc..00000000 --- a/src/main/deploy/buttercup_swerve/cameras/shooter.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "shooter", - "use": "target", - "type": "photonvision", - "column": 0, - "x": -0.25, - "y": 0, - "z": 0.25, - "roll": 0, - "pitch": 20, - "yaw": 180, - "targetFiducialIds": [ - 4, - 11 - ] -} diff --git a/src/main/deploy/buttercup_swerve/competition_mode.json b/src/main/deploy/buttercup_swerve/competition_mode.json deleted file mode 100644 index f67b949b..00000000 --- a/src/main/deploy/buttercup_swerve/competition_mode.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "maxSpeed": 6.0, - "maxAngularSpeed": 6.0, - "maxAccelleration": 3.0, - "maxAngularAccelleration": 18.75 -} diff --git a/src/main/deploy/buttercup_swerve/controllers.json b/src/main/deploy/buttercup_swerve/controllers.json deleted file mode 100644 index f6fc6204..00000000 --- a/src/main/deploy/buttercup_swerve/controllers.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "controllers": [ - "driver.json" - - ] -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json deleted file mode 100644 index 41b79133..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 2, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json deleted file mode 100644 index d77f0e91..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 0, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json deleted file mode 100644 index d826731e..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 1, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json deleted file mode 100644 index 47b46917..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 3, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json deleted file mode 100644 index 8f981e23..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 4, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json b/src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json deleted file mode 100644 index c13bd0e0..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "channel": 1, - "deadband": 0.07, - "invert": "true", - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0, - "rate": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json b/src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json deleted file mode 100644 index 2668956f..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "channel": 5, - "deadband": 0.07, - "invert": "true", - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0, - "rate": 1.0 -} diff --git a/src/main/deploy/buttercup_swerve/controllers/driver.json b/src/main/deploy/buttercup_swerve/controllers/driver.json deleted file mode 100644 index e2dbc76a..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/driver.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "driver", - "port": 0, - "axis": [ - "driver_left_y.json", - "driver_left_x.json", - "driver_right_x.json", - "driver_left_trigger.json", - "driver_right_trigger.json" - ] -} diff --git a/src/main/deploy/buttercup_swerve/controllers/operator.json b/src/main/deploy/buttercup_swerve/controllers/operator.json deleted file mode 100644 index 858cfea8..00000000 --- a/src/main/deploy/buttercup_swerve/controllers/operator.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "operator", - "port": 1, - "axis": [ - "operator_left_y.json", - "operator_right_y.json" - ] -} diff --git a/src/main/deploy/buttercup_swerve/demo_mode.json b/src/main/deploy/buttercup_swerve/demo_mode.json deleted file mode 100644 index f60f73c9..00000000 --- a/src/main/deploy/buttercup_swerve/demo_mode.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "maxSpeed": 2.0, - "maxAngularSpeed": 2.0, - "maxAccelleration": 1.0, - "maxAngularAccelleration": 9.0 -} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/backleft.json b/src/main/deploy/buttercup_swerve/drive_modules/backleft.json deleted file mode 100644 index c62ce932..00000000 --- a/src/main/deploy/buttercup_swerve/drive_modules/backleft.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.19542, - "v": 2.2924, - "a": 0.35934 -} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/backright.json b/src/main/deploy/buttercup_swerve/drive_modules/backright.json deleted file mode 100644 index aee7fd95..00000000 --- a/src/main/deploy/buttercup_swerve/drive_modules/backright.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.18096, - "v": 2.2915, - "a": 0.37156 -} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/frontleft.json b/src/main/deploy/buttercup_swerve/drive_modules/frontleft.json deleted file mode 100644 index bdf7d3fa..00000000 --- a/src/main/deploy/buttercup_swerve/drive_modules/frontleft.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.21848, - "v": 2.3118, - "a": 0.20314 -} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/frontright.json b/src/main/deploy/buttercup_swerve/drive_modules/frontright.json deleted file mode 100644 index 36a1c209..00000000 --- a/src/main/deploy/buttercup_swerve/drive_modules/frontright.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.18434, - "v": 2.3018, - "a": 0.30992 -} diff --git a/src/main/deploy/buttercup_swerve/field/game_pieces.json b/src/main/deploy/buttercup_swerve/field/game_pieces.json deleted file mode 100644 index b13b37c1..00000000 --- a/src/main/deploy/buttercup_swerve/field/game_pieces.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gamePieces": [ - { - "x": 2, - "y": 7, - "type": "Coral", - "rotation": 0 - } - ] -} diff --git a/src/main/deploy/buttercup_swerve/robot.json b/src/main/deploy/buttercup_swerve/robot.json deleted file mode 100644 index 0ced2872..00000000 --- a/src/main/deploy/buttercup_swerve/robot.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "userConfig": "competition_mode.json", - "driveType": "YAGSL_SWERVE_DRIVE", - "trackWidth": 16.8, - "trackWidthUom": "in", - "wheelBase": 18.5, - "wheelBaseUom": "in", - "wheelDiameter": 0.103386904, - "wheelDiameterUom": "m", - "physicalMaxSpeed": 5.93, - "physicalMaxSpeedUom": "m/s", - "driveMotorGearRatio": 1.0, - "loadSimulatedField": false, - "gamePieceA": "Coral", - "gamePieceB": "Algae" -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_drivetrain.json b/src/main/deploy/buttercup_swerve/yagsl_drivetrain.json deleted file mode 100644 index c6beb5d7..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_drivetrain.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "directory": "basic_robot/yagsl_swerve", - "turningMotorGearRatio": 1.0, - "driveModules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ], - "startingPose": { - "x": { - "val": 1.0, - "uom": "m" - }, - "y": { - "val": 1.0, - "uom": "m" - }, - "rotation": { - "val": 0.0, - "uom": "deg" - } - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json deleted file mode 100644 index dd353caf..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "angleJoystickRadiusDeadband": 0.05, - "heading": { - "p": 0.4, - "i": 0, - "d": 0.01 - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json deleted file mode 100644 index 2ac2a1ab..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 17, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 6, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": -8.37, - "left": 10.88 - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json deleted file mode 100644 index eea317b8..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 18, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 2, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": -8.37, - "left": -10.88 - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json deleted file mode 100644 index 1c03c3f1..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 15, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 4, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": 8.37, - "left": 10.88 - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json deleted file mode 100644 index 6218565f..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 16, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 3, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": 8.37, - "left": -10.88 - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json deleted file mode 100644 index c921b000..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "conversionFactors": { - "angle": { - "gearRatio": 12.8, - "factor": 0 - }, - "drive": { - "gearRatio": 8.14, - "diameter": 4, - "factor": 0 - } - }, - "currentLimit": { - "drive": 100, - "angle": 20 - }, - "rampRate": { - "drive": 0.15, - "angle": 0.05 - }, - "wheelGripCoefficientOfFriction": 1.19, - "optimalVoltage": 12, - "robotMass": 75 -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json deleted file mode 100644 index 5924fdbb..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "drive": { - "p": 0.00023, - "i": 0.0000002, - "d": 1, - "f": 0, - "iz": 0 - }, - "angle": { - "p": 0.0020645, - "i": 0, - "d": 0, - "f": 0.001, - "iz": 0 - } -} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json deleted file mode 100644 index f4876233..00000000 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "imu": { - "type": "pigeon2", - "id": 13, - "canbus": "" - }, - "invertedIMU": false, - "modules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ] -} diff --git a/src/main/deploy/pathplanner/settings.json b/src/main/deploy/pathplanner/settings.json index 79b785f6..7642c343 100644 --- a/src/main/deploy/pathplanner/settings.json +++ b/src/main/deploy/pathplanner/settings.json @@ -31,4 +31,4 @@ "robotFeatures": [ "{\"name\":\"Rectangle\",\"type\":\"rounded_rect\",\"data\":{\"center\":{\"x\":0.3,\"y\":0.0},\"size\":{\"width\":0.2,\"length\":0.2},\"borderRadius\":0.05,\"strokeWidth\":0.02,\"filled\":true}}" ] -} \ No newline at end of file +} From 6b88da14ad57857152383eb368d9ebffd7060202 Mon Sep 17 00:00:00 2001 From: Curt Date: Tue, 7 Oct 2025 12:30:50 -0400 Subject: [PATCH 03/93] Squashed commit of the following: commit b7f64a13ba7f5b114331ca3b57a4c4d99f29faf7 Author: Curt Date: Sun Oct 5 15:10:09 2025 -0400 Remove butterbup commit 98e3a69dba9f5e006c9f150cb6ed88f782eae214 Merge: 80b8424 89a320a Author: Curt Date: Sun Oct 5 14:50:00 2025 -0400 Merge branch 'main' into 7-swerve-updates commit 80b84243f77c1c1aba47024f289b81d01de70be6 Author: CameronSkea Date: Wed Oct 1 17:34:36 2025 -0400 updates commit 34af230763e749d8867c0f36a9b716c3945a1af8 Merge: 32ac53c 9c5d9d2 Author: 5010 Tiger Dynasty Date: Wed Oct 1 17:06:27 2025 -0400 Merge pull request #10 from FRC5010/main Merge fixes from main --- .vscode/settings.json | 3 +- simgui.json | 1 + .../black_robot/akit_swerve_drivetrain.json | 3 + src/main/deploy/black_robot/cameras.json | 10 +++ .../deploy/black_robot/cameras/intake.json | 13 ++++ .../black_robot/cameras/localization.json | 13 ++++ .../deploy/black_robot/cameras/quest.json | 11 ++++ .../deploy/black_robot/cameras/shooter.json | 16 +++++ .../deploy/black_robot/competition_mode.json | 6 ++ src/main/deploy/black_robot/controllers.json | 6 ++ .../controllers/axis/driver_left_trigger.json | 8 +++ .../controllers/axis/driver_left_x.json | 8 +++ .../controllers/axis/driver_left_y.json | 8 +++ .../axis/driver_right_trigger.json | 8 +++ .../controllers/axis/driver_right_x.json | 8 +++ .../controllers/axis/operator_left_y.json | 9 +++ .../controllers/axis/operator_right_y.json | 9 +++ .../black_robot/controllers/driver.json | 11 ++++ .../black_robot/controllers/operator.json | 8 +++ src/main/deploy/black_robot/demo_mode.json | 6 ++ .../black_robot/drive_modules/backleft.json | 5 ++ .../black_robot/drive_modules/backright.json | 5 ++ .../black_robot/drive_modules/frontleft.json | 5 ++ .../black_robot/drive_modules/frontright.json | 5 ++ .../deploy/black_robot/field/game_pieces.json | 10 +++ src/main/deploy/black_robot/robot.json | 16 +++++ .../subsystems/example/percent_motor.json | 11 ++++ .../subsystems/example/velocity_motor.json | 18 ++++++ .../subsystems/example/yams_arm.json | 64 +++++++++++++++++++ .../subsystems/example/yams_elevator.json | 59 +++++++++++++++++ .../subsystems/example/yams_pivot.json | 57 +++++++++++++++++ .../deploy/black_robot/yagsl_drivetrain.json | 24 +++++++ .../yagsl_swerve/controllerproperties.json | 8 +++ .../yagsl_swerve/modules/backleft.json | 26 ++++++++ .../yagsl_swerve/modules/backright.json | 26 ++++++++ .../yagsl_swerve/modules/frontleft.json | 26 ++++++++ .../yagsl_swerve/modules/frontright.json | 26 ++++++++ .../modules/physicalproperties.json | 24 +++++++ .../yagsl_swerve/modules/pidfproperties.json | 16 +++++ .../black_robot/yagsl_swerve/swervedrive.json | 14 ++++ 40 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 src/main/deploy/black_robot/akit_swerve_drivetrain.json create mode 100644 src/main/deploy/black_robot/cameras.json create mode 100644 src/main/deploy/black_robot/cameras/intake.json create mode 100644 src/main/deploy/black_robot/cameras/localization.json create mode 100644 src/main/deploy/black_robot/cameras/quest.json create mode 100644 src/main/deploy/black_robot/cameras/shooter.json create mode 100644 src/main/deploy/black_robot/competition_mode.json create mode 100644 src/main/deploy/black_robot/controllers.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_left_x.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_left_y.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json create mode 100644 src/main/deploy/black_robot/controllers/axis/driver_right_x.json create mode 100644 src/main/deploy/black_robot/controllers/axis/operator_left_y.json create mode 100644 src/main/deploy/black_robot/controllers/axis/operator_right_y.json create mode 100644 src/main/deploy/black_robot/controllers/driver.json create mode 100644 src/main/deploy/black_robot/controllers/operator.json create mode 100644 src/main/deploy/black_robot/demo_mode.json create mode 100644 src/main/deploy/black_robot/drive_modules/backleft.json create mode 100644 src/main/deploy/black_robot/drive_modules/backright.json create mode 100644 src/main/deploy/black_robot/drive_modules/frontleft.json create mode 100644 src/main/deploy/black_robot/drive_modules/frontright.json create mode 100644 src/main/deploy/black_robot/field/game_pieces.json create mode 100644 src/main/deploy/black_robot/robot.json create mode 100644 src/main/deploy/black_robot/subsystems/example/percent_motor.json create mode 100644 src/main/deploy/black_robot/subsystems/example/velocity_motor.json create mode 100644 src/main/deploy/black_robot/subsystems/example/yams_arm.json create mode 100644 src/main/deploy/black_robot/subsystems/example/yams_elevator.json create mode 100644 src/main/deploy/black_robot/subsystems/example/yams_pivot.json create mode 100644 src/main/deploy/black_robot/yagsl_drivetrain.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/backright.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json create mode 100644 src/main/deploy/black_robot/yagsl_swerve/swervedrive.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a9e0e63..3b81250d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,6 @@ null ], "java.test.defaultConfig": "WPIlibUnitTests", - "java.debug.settings.onBuildFailureProceed": true + "java.debug.settings.onBuildFailureProceed": true, + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable" } diff --git a/simgui.json b/simgui.json index 2d7c3046..c0095c86 100644 --- a/simgui.json +++ b/simgui.json @@ -70,6 +70,7 @@ "/LiveWindow/Ungrouped/Pigeon 2 [13]": "Gyro", "/LiveWindow/Ungrouped/Scheduler": "Scheduler", "/Shuffleboard/BabySwerve/Auto Modes": "String Chooser", + "/Shuffleboard/BlackRobot/Auto Modes": "String Chooser", "/Shuffleboard/ExampleRobot/Auto Modes": "String Chooser", "/SmartDashboard/Alerts": "Alerts", "/SmartDashboard/Arm/mechanism": "Mechanism2d", diff --git a/src/main/deploy/black_robot/akit_swerve_drivetrain.json b/src/main/deploy/black_robot/akit_swerve_drivetrain.json new file mode 100644 index 00000000..42f49455 --- /dev/null +++ b/src/main/deploy/black_robot/akit_swerve_drivetrain.json @@ -0,0 +1,3 @@ +{ + "type": "SparkTalon" +} diff --git a/src/main/deploy/black_robot/cameras.json b/src/main/deploy/black_robot/cameras.json new file mode 100644 index 00000000..1d32743e --- /dev/null +++ b/src/main/deploy/black_robot/cameras.json @@ -0,0 +1,10 @@ +{ + "cameras": [ + "intake.json", + "localization.json", + "shooter.json", + "quest.json" + ], + "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", + "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" +} diff --git a/src/main/deploy/black_robot/cameras/intake.json b/src/main/deploy/black_robot/cameras/intake.json new file mode 100644 index 00000000..33938add --- /dev/null +++ b/src/main/deploy/black_robot/cameras/intake.json @@ -0,0 +1,13 @@ +{ + "name": "intake", + "use": "target", + "type": "limelight", + "column": 0, + "x": 0.15, + "y": 0.05, + "z": 0.2, + "roll": 0, + "pitch": 10, + "yaw": 0, + "targetHeight": 0.02 +} diff --git a/src/main/deploy/black_robot/cameras/localization.json b/src/main/deploy/black_robot/cameras/localization.json new file mode 100644 index 00000000..706b561a --- /dev/null +++ b/src/main/deploy/black_robot/cameras/localization.json @@ -0,0 +1,13 @@ +{ + "name": "localization", + "use": "apriltag", + "type": "limelight", + "strategy": "LOWEST_AMBIGUITY", + "column": 0, + "x": -0.35, + "y": -0.25, + "z": 0.5, + "roll": 0, + "pitch": -20, + "yaw": 180 +} diff --git a/src/main/deploy/black_robot/cameras/quest.json b/src/main/deploy/black_robot/cameras/quest.json new file mode 100644 index 00000000..df412311 --- /dev/null +++ b/src/main/deploy/black_robot/cameras/quest.json @@ -0,0 +1,11 @@ +{ + "name": "quest", + "use": "quest", + "column": 0, + "x": 0, + "y": 0, + "z": 18, + "roll": 0, + "pitch": 0, + "yaw": 0 +} diff --git a/src/main/deploy/black_robot/cameras/shooter.json b/src/main/deploy/black_robot/cameras/shooter.json new file mode 100644 index 00000000..b60f68bc --- /dev/null +++ b/src/main/deploy/black_robot/cameras/shooter.json @@ -0,0 +1,16 @@ +{ + "name": "shooter", + "use": "target", + "type": "photonvision", + "column": 0, + "x": -0.25, + "y": 0, + "z": 0.25, + "roll": 0, + "pitch": 20, + "yaw": 180, + "targetFiducialIds": [ + 4, + 11 + ] +} diff --git a/src/main/deploy/black_robot/competition_mode.json b/src/main/deploy/black_robot/competition_mode.json new file mode 100644 index 00000000..f67b949b --- /dev/null +++ b/src/main/deploy/black_robot/competition_mode.json @@ -0,0 +1,6 @@ +{ + "maxSpeed": 6.0, + "maxAngularSpeed": 6.0, + "maxAccelleration": 3.0, + "maxAngularAccelleration": 18.75 +} diff --git a/src/main/deploy/black_robot/controllers.json b/src/main/deploy/black_robot/controllers.json new file mode 100644 index 00000000..c6c86acb --- /dev/null +++ b/src/main/deploy/black_robot/controllers.json @@ -0,0 +1,6 @@ +{ + "controllers": [ + "driver.json", + "operator.json" + ] +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json new file mode 100644 index 00000000..41b79133 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json @@ -0,0 +1,8 @@ +{ + "channel": 2, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_x.json b/src/main/deploy/black_robot/controllers/axis/driver_left_x.json new file mode 100644 index 00000000..d77f0e91 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_x.json @@ -0,0 +1,8 @@ +{ + "channel": 0, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_y.json b/src/main/deploy/black_robot/controllers/axis/driver_left_y.json new file mode 100644 index 00000000..d826731e --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_y.json @@ -0,0 +1,8 @@ +{ + "channel": 1, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json new file mode 100644 index 00000000..47b46917 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json @@ -0,0 +1,8 @@ +{ + "channel": 3, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/driver_right_x.json b/src/main/deploy/black_robot/controllers/axis/driver_right_x.json new file mode 100644 index 00000000..8f981e23 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/driver_right_x.json @@ -0,0 +1,8 @@ +{ + "channel": 4, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/operator_left_y.json b/src/main/deploy/black_robot/controllers/axis/operator_left_y.json new file mode 100644 index 00000000..c13bd0e0 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/operator_left_y.json @@ -0,0 +1,9 @@ +{ + "channel": 1, + "deadband": 0.07, + "invert": "true", + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0, + "rate": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/axis/operator_right_y.json b/src/main/deploy/black_robot/controllers/axis/operator_right_y.json new file mode 100644 index 00000000..2668956f --- /dev/null +++ b/src/main/deploy/black_robot/controllers/axis/operator_right_y.json @@ -0,0 +1,9 @@ +{ + "channel": 5, + "deadband": 0.07, + "invert": "true", + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0, + "rate": 1.0 +} diff --git a/src/main/deploy/black_robot/controllers/driver.json b/src/main/deploy/black_robot/controllers/driver.json new file mode 100644 index 00000000..e2dbc76a --- /dev/null +++ b/src/main/deploy/black_robot/controllers/driver.json @@ -0,0 +1,11 @@ +{ + "name": "driver", + "port": 0, + "axis": [ + "driver_left_y.json", + "driver_left_x.json", + "driver_right_x.json", + "driver_left_trigger.json", + "driver_right_trigger.json" + ] +} diff --git a/src/main/deploy/black_robot/controllers/operator.json b/src/main/deploy/black_robot/controllers/operator.json new file mode 100644 index 00000000..858cfea8 --- /dev/null +++ b/src/main/deploy/black_robot/controllers/operator.json @@ -0,0 +1,8 @@ +{ + "name": "operator", + "port": 1, + "axis": [ + "operator_left_y.json", + "operator_right_y.json" + ] +} diff --git a/src/main/deploy/black_robot/demo_mode.json b/src/main/deploy/black_robot/demo_mode.json new file mode 100644 index 00000000..f60f73c9 --- /dev/null +++ b/src/main/deploy/black_robot/demo_mode.json @@ -0,0 +1,6 @@ +{ + "maxSpeed": 2.0, + "maxAngularSpeed": 2.0, + "maxAccelleration": 1.0, + "maxAngularAccelleration": 9.0 +} diff --git a/src/main/deploy/black_robot/drive_modules/backleft.json b/src/main/deploy/black_robot/drive_modules/backleft.json new file mode 100644 index 00000000..c62ce932 --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/backleft.json @@ -0,0 +1,5 @@ +{ + "s": 0.19542, + "v": 2.2924, + "a": 0.35934 +} diff --git a/src/main/deploy/black_robot/drive_modules/backright.json b/src/main/deploy/black_robot/drive_modules/backright.json new file mode 100644 index 00000000..aee7fd95 --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/backright.json @@ -0,0 +1,5 @@ +{ + "s": 0.18096, + "v": 2.2915, + "a": 0.37156 +} diff --git a/src/main/deploy/black_robot/drive_modules/frontleft.json b/src/main/deploy/black_robot/drive_modules/frontleft.json new file mode 100644 index 00000000..bdf7d3fa --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/frontleft.json @@ -0,0 +1,5 @@ +{ + "s": 0.21848, + "v": 2.3118, + "a": 0.20314 +} diff --git a/src/main/deploy/black_robot/drive_modules/frontright.json b/src/main/deploy/black_robot/drive_modules/frontright.json new file mode 100644 index 00000000..36a1c209 --- /dev/null +++ b/src/main/deploy/black_robot/drive_modules/frontright.json @@ -0,0 +1,5 @@ +{ + "s": 0.18434, + "v": 2.3018, + "a": 0.30992 +} diff --git a/src/main/deploy/black_robot/field/game_pieces.json b/src/main/deploy/black_robot/field/game_pieces.json new file mode 100644 index 00000000..b13b37c1 --- /dev/null +++ b/src/main/deploy/black_robot/field/game_pieces.json @@ -0,0 +1,10 @@ +{ + "gamePieces": [ + { + "x": 2, + "y": 7, + "type": "Coral", + "rotation": 0 + } + ] +} diff --git a/src/main/deploy/black_robot/robot.json b/src/main/deploy/black_robot/robot.json new file mode 100644 index 00000000..0ced2872 --- /dev/null +++ b/src/main/deploy/black_robot/robot.json @@ -0,0 +1,16 @@ +{ + "userConfig": "competition_mode.json", + "driveType": "YAGSL_SWERVE_DRIVE", + "trackWidth": 16.8, + "trackWidthUom": "in", + "wheelBase": 18.5, + "wheelBaseUom": "in", + "wheelDiameter": 0.103386904, + "wheelDiameterUom": "m", + "physicalMaxSpeed": 5.93, + "physicalMaxSpeedUom": "m/s", + "driveMotorGearRatio": 1.0, + "loadSimulatedField": false, + "gamePieceA": "Coral", + "gamePieceB": "Algae" +} diff --git a/src/main/deploy/black_robot/subsystems/example/percent_motor.json b/src/main/deploy/black_robot/subsystems/example/percent_motor.json new file mode 100644 index 00000000..027c231f --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/percent_motor.json @@ -0,0 +1,11 @@ +{ + "name": "percent_motor", + "controller": "spark", + "type": "Neo", + "id": 11, + "gearing": 1.0, + "momentOfInertiaKgMSq": 1.0, + "x": 0.5, + "y": 0.0, + "z": 0.25 +} diff --git a/src/main/deploy/black_robot/subsystems/example/velocity_motor.json b/src/main/deploy/black_robot/subsystems/example/velocity_motor.json new file mode 100644 index 00000000..185de2e0 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/velocity_motor.json @@ -0,0 +1,18 @@ +{ + "name": "velocity_motor", + "controller": "spark", + "type": "Neo", + "id": 14, + "gearing": 1.0, + "momentOfInertiaKgMSq": 1.0, + "x": -0.25, + "y": 0.0, + "z": 0.25, + "kP": 0.1, + "kI": 0.0, + "kD": 0.01, + "iZone": 0.0, + "kS": 0.1, + "kV": 0.1, + "kA": 0.0 +} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_arm.json b/src/main/deploy/black_robot/subsystems/example/yams_arm.json new file mode 100644 index 00000000..38cf9e27 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/yams_arm.json @@ -0,0 +1,64 @@ +{ + "motorSetup": { + "name": "Arm", + "controllerType": "talonfx", + "motorType": "KrakenX60", + "canId": 11 + }, + "motorSystemId": { + "feedBack": { + "p": 4, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + }, + "maxVelocity": { + "val": 180, + "uom": "deg/s" + }, + "maxAcceleration": { + "val": 90, + "uom": "deg/s^2" + } + }, + "length": { + "val": 0.135, + "uom": "in" + }, + "lowerHardLimit": { + "val": -80, + "uom": "deg" + }, + "upperHardLimit": { + "val": 200, + "uom": "deg" + }, + "startingAngle": { + "val": 0, + "uom": "deg" + }, + "lowerSoftLimit": { + "val": -30, + "uom": "deg" + }, + "upperSoftLimit": { + "val": 100, + "uom": "deg" + }, + "gearing": [ + 3, + 4 + ], + "mass": { + "val": 1, + "uom": "lbs" + }, + "horizontalZero": { + "val": 0, + "uom": "deg" + } +} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_elevator.json b/src/main/deploy/black_robot/subsystems/example/yams_elevator.json new file mode 100644 index 00000000..55046e05 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/yams_elevator.json @@ -0,0 +1,59 @@ +{ + "motorSetup": { + "name": "Elevator", + "controllerType": "spark", + "motorType": "Neo", + "canId": 10 + }, + "motorSystemId": { + "feedBack": { + "p": 2.7739E-10, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.013799, + "g": 0.24168, + "v": 10.219, + "a": 0.2153 + }, + "maxVelocity": { + "val": 4, + "uom": "m/s" + }, + "maxAcceleration": { + "val": 20, + "uom": "m/s^2" + }, + "controlMode": "OPEN_LOOP" + }, + "sprocketTeeth": 22, + "lowerSoftLimit": { + "val": 0.1, + "uom": "m" + }, + "upperSoftLimit": { + "val": 2, + "uom": "m" + }, + "lowerHardLimit": { + "val": 0.1, + "uom": "m" + }, + "upperHardLimit": { + "val": 3, + "uom": "m" + }, + "gearing": [ + 3, + 4 + ], + "startingPosition": { + "val": 0.5, + "uom": "m" + }, + "mass": { + "val": 16, + "uom": "lbs" + } +} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_pivot.json b/src/main/deploy/black_robot/subsystems/example/yams_pivot.json new file mode 100644 index 00000000..329453e1 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/example/yams_pivot.json @@ -0,0 +1,57 @@ +{ + "motorSetup": { + "name": "Turret", + "controllerType": "talonfx", + "motorType": "KrakenX60", + "canId": 12 + }, + "motorSystemId": { + "feedBack": { + "p": 4, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + }, + "maxVelocity": { + "val": 180, + "uom": "deg/s" + }, + "maxAcceleration": { + "val": 90, + "uom": "deg/s^2" + } + }, + "lowerHardLimit": { + "val": -100, + "uom": "deg" + }, + "upperHardLimit": { + "val": 200, + "uom": "deg" + }, + "startingAngle": { + "val": 0, + "uom": "deg" + }, + "lowerSoftLimit": { + "val": -30, + "uom": "deg" + }, + "upperSoftLimit": { + "val": 100, + "uom": "deg" + }, + "gearing": [ + 3, + 4 + ], + "startingPosition": { + "val": 0, + "uom": "deg" + }, + "moi": 0.001 +} diff --git a/src/main/deploy/black_robot/yagsl_drivetrain.json b/src/main/deploy/black_robot/yagsl_drivetrain.json new file mode 100644 index 00000000..c6beb5d7 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_drivetrain.json @@ -0,0 +1,24 @@ +{ + "directory": "basic_robot/yagsl_swerve", + "turningMotorGearRatio": 1.0, + "driveModules": [ + "frontleft.json", + "frontright.json", + "backleft.json", + "backright.json" + ], + "startingPose": { + "x": { + "val": 1.0, + "uom": "m" + }, + "y": { + "val": 1.0, + "uom": "m" + }, + "rotation": { + "val": 0.0, + "uom": "deg" + } + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json b/src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json new file mode 100644 index 00000000..dd353caf --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/controllerproperties.json @@ -0,0 +1,8 @@ +{ + "angleJoystickRadiusDeadband": 0.05, + "heading": { + "p": 0.4, + "i": 0, + "d": 0.01 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json new file mode 100644 index 00000000..2ac2a1ab --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 17, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 6, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": -8.37, + "left": 10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json new file mode 100644 index 00000000..eea317b8 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 18, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 2, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": -8.37, + "left": -10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json new file mode 100644 index 00000000..1c03c3f1 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 15, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 4, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": 8.37, + "left": 10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json new file mode 100644 index 00000000..6218565f --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -0,0 +1,26 @@ +{ + "drive": { + "type": "neo", + "id": 16, + "canbus": null + }, + "angle": { + "type": "neo", + "id": 3, + "canbus": null + }, + "encoder": { + "type": "canandcoder", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": true + }, + "absoluteEncoderInverted": true, + "location": { + "front": 8.37, + "left": -10.88 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json new file mode 100644 index 00000000..c921b000 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json @@ -0,0 +1,24 @@ +{ + "conversionFactors": { + "angle": { + "gearRatio": 12.8, + "factor": 0 + }, + "drive": { + "gearRatio": 8.14, + "diameter": 4, + "factor": 0 + } + }, + "currentLimit": { + "drive": 100, + "angle": 20 + }, + "rampRate": { + "drive": 0.15, + "angle": 0.05 + }, + "wheelGripCoefficientOfFriction": 1.19, + "optimalVoltage": 12, + "robotMass": 75 +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json new file mode 100644 index 00000000..5924fdbb --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json @@ -0,0 +1,16 @@ +{ + "drive": { + "p": 0.00023, + "i": 0.0000002, + "d": 1, + "f": 0, + "iz": 0 + }, + "angle": { + "p": 0.0020645, + "i": 0, + "d": 0, + "f": 0.001, + "iz": 0 + } +} diff --git a/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json b/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json new file mode 100644 index 00000000..f4876233 --- /dev/null +++ b/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json @@ -0,0 +1,14 @@ +{ + "imu": { + "type": "pigeon2", + "id": 13, + "canbus": "" + }, + "invertedIMU": false, + "modules": [ + "frontleft.json", + "frontright.json", + "backleft.json", + "backright.json" + ] +} From b1cc0c2da00fb5aedde6bf095397905cb3c212a0 Mon Sep 17 00:00:00 2001 From: Curt Date: Tue, 7 Oct 2025 12:38:17 -0400 Subject: [PATCH 04/93] Reinstate BlackRobot --- src/main/deploy/robots.json | 7 +++- src/main/java/frc/robot/BuildConstants.java | 14 +++---- .../java/frc/robot/blackteam/BlackRobot.java | 42 +++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 src/main/java/frc/robot/blackteam/BlackRobot.java diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 7361d8ad..7121e5e3 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -4,11 +4,16 @@ "basic_robot": { "id": "basic_robot", "robotClass": "frc.robot.example.ExampleRobot", - "simulate": true + "simulate": false }, "baby_swerve": { "id": "baby_swerve", "robotClass": "frc.robot.baby_swerve.BabySwerve", + "simulate": false + }, + "black_robot": { + "id": "black_robot", + "robotClass": "frc.robot.blackteam.BlackRobot", "simulate": true, "competition": true } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index df08d5e4..c9e4a4ae 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -3,14 +3,14 @@ /** Automatically generated file containing build version information. */ public final class BuildConstants { public static final String MAVEN_GROUP = ""; - public static final String MAVEN_NAME = "FRC5010Example"; + public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 28; - public static final String GIT_SHA = "7cb77ee59bae63f552afeda16867765ac1d22e2b"; - public static final String GIT_DATE = "2025-10-05 14:47:51 EDT"; - public static final String GIT_BRANCH = "main"; - public static final String BUILD_DATE = "2025-10-07 10:38:30 EDT"; - public static final long BUILD_UNIX_TIME = 1759847910272L; + public static final int GIT_REVISION = 34; + public static final String GIT_SHA = "6b88da14ad57857152383eb368d9ebffd7060202"; + public static final String GIT_DATE = "2025-10-07 12:30:50 EDT"; + public static final String GIT_BRANCH = "LobbinLoco"; + public static final String BUILD_DATE = "2025-10-07 12:36:50 EDT"; + public static final long BUILD_UNIX_TIME = 1759855010411L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java new file mode 100644 index 00000000..9b6ada01 --- /dev/null +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -0,0 +1,42 @@ +package frc.robot.blackteam; + +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import org.frc5010.common.arch.GenericRobot; +import org.frc5010.common.config.ConfigConstants; +import org.frc5010.common.drive.GenericDrivetrain; +import org.frc5010.common.sensors.Controller; + +public class BlackRobot extends GenericRobot { + private GenericDrivetrain drivetrain; + + public BlackRobot(String directory) { + super(directory); + drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); + } + + @Override + public void configureButtonBindings(Controller driver, Controller operator) {} + + @Override + public void setupDefaultCommands(Controller driver, Controller operator) { + driver.setRightTrigger(driver.createRightTrigger()); + drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); + } + + @Override + public void initAutoCommands() { + drivetrain.setAutoBuilder(); + } + + @Override + public Command generateAutoCommand(Command autoCommand) { + return drivetrain.generateAutoCommand(autoCommand); + } + + @Override + public void buildAutoCommands() { + super.buildAutoCommands(); + selectableCommand.addOption("Do Nothing", Commands.none()); + } +} From e13250ec939b8af150519207471e85fec1da373c Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Thu, 9 Oct 2025 18:31:57 -0400 Subject: [PATCH 05/93] for robert for robert --- src/main/deploy/pathplanner/navgrid.json | 2089 ++++++++++++++++- src/main/deploy/robots.json | 2 +- src/main/java/frc/robot/BuildConstants.java | 10 +- .../java/frc/robot/blackteam/BlackRobot.java | 6 +- .../frc/robot/blackteam/ShooterSubsystem.java | 69 + 5 files changed, 2168 insertions(+), 8 deletions(-) create mode 100644 src/main/java/frc/robot/blackteam/ShooterSubsystem.java diff --git a/src/main/deploy/pathplanner/navgrid.json b/src/main/deploy/pathplanner/navgrid.json index ea670052..6e83d807 100644 --- a/src/main/deploy/pathplanner/navgrid.json +++ b/src/main/deploy/pathplanner/navgrid.json @@ -1 +1,2088 @@ -{"field_size":{"x":15.24,"y":8.25},"nodeSizeMeters":0.25,"grid":[[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]]} \ No newline at end of file +{ + "field_size": { + "x": 15.24, + "y": 8.25 + }, + "nodeSizeMeters": 0.25, + "grid": [ + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + ] +} diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 9fdbd10c..f31fa954 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -4,7 +4,7 @@ "basic_robot": { "id": "basic_robot", "robotClass": "frc.robot.example.ExampleRobot", - "simulate": true + "simulate": false }, "black_robot": { "id": "black_robot", diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index c1ca3a51..2409b111 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 18; - public static final String GIT_SHA = "34af230763e749d8867c0f36a9b716c3945a1af8"; - public static final String GIT_DATE = "2025-10-01 17:06:27 EDT"; + public static final int GIT_REVISION = 33; + public static final String GIT_SHA = "b7f64a13ba7f5b114331ca3b57a4c4d99f29faf7"; + public static final String GIT_DATE = "2025-10-05 15:10:09 EDT"; public static final String GIT_BRANCH = "7-swerve-updates"; - public static final String BUILD_DATE = "2025-10-01 17:32:55 EDT"; - public static final long BUILD_UNIX_TIME = 1759354375311L; + public static final String BUILD_DATE = "2025-10-09 18:30:30 EDT"; + public static final long BUILD_UNIX_TIME = 1760049030617L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 9b6ada01..2d2980db 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -9,14 +9,18 @@ public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; + private ShooterSubsystem shooterSubsystem; public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); + shooterSubsystem = new ShooterSubsystem(); } @Override - public void configureButtonBindings(Controller driver, Controller operator) {} + public void configureButtonBindings(Controller driver, Controller operator) { + driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); + } @Override public void setupDefaultCommands(Controller driver, Controller operator) { diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java new file mode 100644 index 00000000..fc56ecda --- /dev/null +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -0,0 +1,69 @@ +package frc.robot.blackteam; + +import static edu.wpi.first.units.Units.Amps; +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.DegreesPerSecondPerSecond; +import static edu.wpi.first.units.Units.Inches; +import static edu.wpi.first.units.Units.Pounds; +import static edu.wpi.first.units.Units.RPM; +import static edu.wpi.first.units.Units.Seconds; +import static yams.mechanisms.SmartMechanism.gearbox; +import static yams.mechanisms.SmartMechanism.gearing; + +import com.thethriftybot.ThriftyNova; +import edu.wpi.first.math.controller.ArmFeedforward; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj2.command.Command; +import org.frc5010.common.arch.GenericSubsystem; +import yams.mechanisms.config.ShooterConfig; +import yams.mechanisms.velocity.Shooter; +import yams.motorcontrollers.SmartMotorController; +import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; +import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; +import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; +import yams.motorcontrollers.local.NovaWrapper; + +public class ShooterSubsystem extends GenericSubsystem { + private final ThriftyNova motor = new ThriftyNova(10); + private final SmartMotorControllerConfig motorConfig = + new SmartMotorControllerConfig(this) + .withClosedLoopController( + 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.COAST) + .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withClosedLoopRampRate(Seconds.of(0.25)) + .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) + .withControlMode(ControlMode.CLOSED_LOOP); + + private final SmartMotorController motorController = + new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + /** Creates a new Shooter. */ + private final ShooterConfig shooterConfig = + new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); + + private final Shooter shooter = new Shooter(shooterConfig); + + public ShooterSubsystem() {} + + public Command setSpeed(double speed) { + return shooter.set(speed); + } + + @Override + public void periodic() { + shooter.updateTelemetry(); + } + + @Override + public void simulationPeriodic() { + shooter.simIterate(); + } +} From b0d78855eb9f11e406324939938314ebe2e43a1a Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Mon, 20 Oct 2025 18:51:17 -0400 Subject: [PATCH 06/93] new code --- .../frc/robot/blackteam/ShooterSubsystem.java | 76 +++++++++++----- src/main/java/frc/robot/blackteam/feeder.java | 88 +++++++++++++++++++ 2 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 src/main/java/frc/robot/blackteam/feeder.java diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index fc56ecda..c2156e11 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -10,11 +10,20 @@ import static yams.mechanisms.SmartMechanism.gearbox; import static yams.mechanisms.SmartMechanism.gearing; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; + +import org.frc5010.common.arch.GenericSubsystem; + import com.thethriftybot.ThriftyNova; + import edu.wpi.first.math.controller.ArmFeedforward; +import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj2.command.Command; -import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.config.ShooterConfig; import yams.mechanisms.velocity.Shooter; import yams.motorcontrollers.SmartMotorController; @@ -26,37 +35,59 @@ public class ShooterSubsystem extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(10); - private final SmartMotorControllerConfig motorConfig = - new SmartMotorControllerConfig(this) - .withClosedLoopController( - 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) - .withGearing(gearing(gearbox(3, 4))) - .withIdleMode(MotorMode.COAST) - .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withClosedLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) - .withControlMode(ControlMode.CLOSED_LOOP); + private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( + Map.entry(0.0, 0.0), + Map.entry(0.5, 500.0), + Map.entry(1.0, 1000.0), + Map.entry(1.5, 1500.0)); + + private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) + .withClosedLoopController( + 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.COAST) + .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withClosedLoopRampRate(Seconds.of(0.25)) + .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) + .withControlMode(ControlMode.CLOSED_LOOP); - private final SmartMotorController motorController = - new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); /** Creates a new Shooter. */ - private final ShooterConfig shooterConfig = - new ShooterConfig(motorController) - .withDiameter(Inches.of(4)) - .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); + private final ShooterConfig shooterConfig = new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); private final Shooter shooter = new Shooter(shooterConfig); - public ShooterSubsystem() {} + public ShooterSubsystem() { + distanceToVelocityMap.put(0.0, 0.0); + distanceToVelocityMap.put(0.5, 500.0); + } public Command setSpeed(double speed) { return shooter.set(speed); } + public Command launchToDistance(DoubleSupplier distanceSupplier) { + return shooter.setSpeed( + () -> RPM.of( + distanceToVelocityMap.get( + distanceSupplier.getAsDouble()))); + + } + public Command spinAtSpeed(DoubleSupplier speedSupplier){ + return shooter.setSpeed(RPM.of(speedSupplier.getAsDouble())); + } +public Supplier getVelocity(){ + return () -> shooter.getSpeed(); +} +public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range){ + return shooter.isNear(expected, range); +} @Override public void periodic() { shooter.updateTelemetry(); @@ -66,4 +97,5 @@ public void periodic() { public void simulationPeriodic() { shooter.simIterate(); } + } diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java new file mode 100644 index 00000000..a5800220 --- /dev/null +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -0,0 +1,88 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.blackteam; + +import org.frc5010.common.arch.GenericSubsystem; + +import com.thethriftybot.ThriftyNova; + + +import static edu.wpi.first.units.Units.Amps; +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.DegreesPerSecondPerSecond; +import static edu.wpi.first.units.Units.Inches; +import static edu.wpi.first.units.Units.Pounds; +import static edu.wpi.first.units.Units.RPM; +import static edu.wpi.first.units.Units.Seconds; +import static yams.mechanisms.SmartMechanism.gearbox; +import static yams.mechanisms.SmartMechanism.gearing; + +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; + +import org.frc5010.common.arch.GenericSubsystem; + +import com.thethriftybot.ThriftyNova; + +import edu.wpi.first.math.controller.ArmFeedforward; +import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj2.command.Command; +import yams.mechanisms.config.ShooterConfig; +import yams.mechanisms.velocity.Shooter; +import yams.motorcontrollers.SmartMotorController; +import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; +import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; +import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; +import yams.motorcontrollers.local.NovaWrapper; +import edu.wpi.first.math.controller.ArmFeedforward; +import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; +import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; +import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; + +/** Add your docs here. */ +public class feeder extends GenericSubsystem { + private final ThriftyNova motor = new ThriftyNova(11); + private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.BRAKE) + .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withOpenLoopRampRate(Seconds.of(0.25)) + .withControlMode(ControlMode.OPEN_LOOP); + + private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + + private final ShooterConfig feederConfig = new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); + + private final Shooter feeder = new Shooter(feederConfig); + + public Command setSpeed(double speed) { + return feeder.set(speed); + } + public feeder(){ + + + } + + @Override + public void periodic() { + feeder.updateTelemetry(); + } + @Override + public void simulationPeriodic() { + feeder.simIterate(); + } +} \ No newline at end of file From 6f9381f001daeef801112984355c78ccafe77ad1 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Thu, 23 Oct 2025 17:43:47 -0400 Subject: [PATCH 07/93] Added New Autos --- simgui.json | 17 ++-- .../{New Auto.auto => BlueToRedAuto.auto} | 2 +- .../pathplanner/autos/RedToBlueAuto.auto | 19 ++++ .../deploy/pathplanner/paths/BlueToRed.path | 54 +++++++++++ .../deploy/pathplanner/paths/New Path.path | 70 -------------- .../deploy/pathplanner/paths/RedToBlue.path | 54 +++++++++++ src/main/java/frc/robot/BuildConstants.java | 10 +- .../frc/robot/blackteam/ShooterSubsystem.java | 85 ++++++++--------- src/main/java/frc/robot/blackteam/feeder.java | 92 ++++++++----------- .../auto/pathplanner/PathFinderCommand.java | 1 - 10 files changed, 218 insertions(+), 186 deletions(-) rename src/main/deploy/pathplanner/autos/{New Auto.auto => BlueToRedAuto.auto} (87%) create mode 100644 src/main/deploy/pathplanner/autos/RedToBlueAuto.auto create mode 100644 src/main/deploy/pathplanner/paths/BlueToRed.path delete mode 100644 src/main/deploy/pathplanner/paths/New Path.path create mode 100644 src/main/deploy/pathplanner/paths/RedToBlue.path diff --git a/simgui.json b/simgui.json index c0095c86..2200d680 100644 --- a/simgui.json +++ b/simgui.json @@ -82,6 +82,7 @@ "/SmartDashboard/Field": "Field2d", "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/LiveTuning": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", @@ -89,6 +90,7 @@ "/SmartDashboard/Pigeon 2 [13]": "Gyro", "/SmartDashboard/Robot Visual": "Mechanism2d", "/SmartDashboard/SendableChooser[0]": "String Chooser", + "/SmartDashboard/ShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/Swerve Drive": "Alerts", "/SmartDashboard/Turret/mechanism": "Mechanism2d", "/SmartDashboard/VisionSystemSim-Vision/Sim Field": "Field2d", @@ -102,7 +104,7 @@ "visible": true } }, - "/Shuffleboard/ExampleRobot/Auto Modes": { + "/Shuffleboard/BlackRobot/Auto Modes": { "window": { "visible": true } @@ -638,11 +640,6 @@ "visible": true } }, - "/SmartDashboard/Elevator/mechanism": { - "window": { - "visible": true - } - }, "/SmartDashboard/Field": { "OdometryPose": { "arrowColor": [ @@ -673,11 +670,6 @@ "visible": true } }, - "/SmartDashboard/Robot Visual": { - "window": { - "visible": true - } - }, "/SmartDashboard/VisionSystemSim-main/Sim Field": { "bottom": 599, "builtin": "", @@ -747,5 +739,8 @@ }, "NetworkTables Info": { "visible": true + }, + "NetworkTables View": { + "visible": false } } diff --git a/src/main/deploy/pathplanner/autos/New Auto.auto b/src/main/deploy/pathplanner/autos/BlueToRedAuto.auto similarity index 87% rename from src/main/deploy/pathplanner/autos/New Auto.auto rename to src/main/deploy/pathplanner/autos/BlueToRedAuto.auto index 268147bb..93a63f8f 100644 --- a/src/main/deploy/pathplanner/autos/New Auto.auto +++ b/src/main/deploy/pathplanner/autos/BlueToRedAuto.auto @@ -7,7 +7,7 @@ { "type": "path", "data": { - "pathName": "New Path" + "pathName": "BlueToRed" } } ] diff --git a/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto b/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto new file mode 100644 index 00000000..e0f02868 --- /dev/null +++ b/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto @@ -0,0 +1,19 @@ +{ + "version": "2025.0", + "command": { + "type": "sequential", + "data": { + "commands": [ + { + "type": "path", + "data": { + "pathName": "RedToBlue" + } + } + ] + } + }, + "resetOdom": true, + "folder": null, + "choreoAuto": false +} \ No newline at end of file diff --git a/src/main/deploy/pathplanner/paths/BlueToRed.path b/src/main/deploy/pathplanner/paths/BlueToRed.path new file mode 100644 index 00000000..f6e1ec0e --- /dev/null +++ b/src/main/deploy/pathplanner/paths/BlueToRed.path @@ -0,0 +1,54 @@ +{ + "version": "2025.0", + "waypoints": [ + { + "anchor": { + "x": 4.61166963755651, + "y": 2.0885799168152124 + }, + "prevControl": null, + "nextControl": { + "x": 4.613482949066363, + "y": 3.2293620876070244 + }, + "isLocked": false, + "linkedName": null + }, + { + "anchor": { + "x": 4.61166963755651, + "y": 4.938966131907309 + }, + "prevControl": { + "x": 4.635625576925734, + "y": 3.7837206284914484 + }, + "nextControl": null, + "isLocked": false, + "linkedName": null + } + ], + "rotationTargets": [], + "constraintZones": [], + "pointTowardsZones": [], + "eventMarkers": [], + "globalConstraints": { + "maxVelocity": 5.25, + "maxAcceleration": 10.0, + "maxAngularVelocity": 540.0, + "maxAngularAcceleration": 720.0, + "nominalVoltage": 12.0, + "unlimited": false + }, + "goalEndState": { + "velocity": 0, + "rotation": 0.8938895949337096 + }, + "reversed": false, + "folder": null, + "idealStartingState": { + "velocity": 0, + "rotation": 0.07192794940851074 + }, + "useDefaultConstraints": true +} \ No newline at end of file diff --git a/src/main/deploy/pathplanner/paths/New Path.path b/src/main/deploy/pathplanner/paths/New Path.path deleted file mode 100644 index 38908444..00000000 --- a/src/main/deploy/pathplanner/paths/New Path.path +++ /dev/null @@ -1,70 +0,0 @@ -{ - "version": "2025.0", - "waypoints": [ - { - "anchor": { - "x": 8.031762295081966, - "y": 6.728227459016393 - }, - "prevControl": null, - "nextControl": { - "x": 6.677151639344262, - "y": 6.728227459016393 - }, - "isLocked": false, - "linkedName": null - }, - { - "anchor": { - "x": 4.207684426229508, - "y": 6.88406762295082 - }, - "prevControl": { - "x": 5.3096147111058976, - "y": 7.435032765389015 - }, - "nextControl": { - "x": 3.8960040983606556, - "y": 6.728227459016393 - }, - "isLocked": false, - "linkedName": null - }, - { - "anchor": { - "x": 3.812090163934426, - "y": 5.217776639344262 - }, - "prevControl": { - "x": 3.57233606557377, - "y": 6.728227459016393 - }, - "nextControl": null, - "isLocked": false, - "linkedName": null - } - ], - "rotationTargets": [], - "constraintZones": [], - "pointTowardsZones": [], - "eventMarkers": [], - "globalConstraints": { - "maxVelocity": 3.0, - "maxAcceleration": 3.0, - "maxAngularVelocity": 540.0, - "maxAngularAcceleration": 720.0, - "nominalVoltage": 12.0, - "unlimited": false - }, - "goalEndState": { - "velocity": 0, - "rotation": -63.43494882292201 - }, - "reversed": false, - "folder": null, - "idealStartingState": { - "velocity": 0, - "rotation": -179.06080905426444 - }, - "useDefaultConstraints": false -} \ No newline at end of file diff --git a/src/main/deploy/pathplanner/paths/RedToBlue.path b/src/main/deploy/pathplanner/paths/RedToBlue.path new file mode 100644 index 00000000..c8b5c03e --- /dev/null +++ b/src/main/deploy/pathplanner/paths/RedToBlue.path @@ -0,0 +1,54 @@ +{ + "version": "2025.0", + "waypoints": [ + { + "anchor": { + "x": 4.696755793226381, + "y": 4.317837195484255 + }, + "prevControl": null, + "nextControl": { + "x": 4.685023010087242, + "y": 1.1560625455389912 + }, + "isLocked": false, + "linkedName": null + }, + { + "anchor": { + "x": 4.696755793226381, + "y": 1.2972786690433742 + }, + "prevControl": { + "x": 4.714172063785987, + "y": 2.651095247012745 + }, + "nextControl": null, + "isLocked": false, + "linkedName": null + } + ], + "rotationTargets": [], + "constraintZones": [], + "pointTowardsZones": [], + "eventMarkers": [], + "globalConstraints": { + "maxVelocity": 5.25, + "maxAcceleration": 10.0, + "maxAngularVelocity": 540.0, + "maxAngularAcceleration": 720.0, + "nominalVoltage": 12.0, + "unlimited": false + }, + "goalEndState": { + "velocity": 0, + "rotation": 0.0 + }, + "reversed": false, + "folder": null, + "idealStartingState": { + "velocity": 0, + "rotation": 0.0 + }, + "useDefaultConstraints": true +} \ No newline at end of file diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index c9e4a4ae..94d2f41e 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 34; - public static final String GIT_SHA = "6b88da14ad57857152383eb368d9ebffd7060202"; - public static final String GIT_DATE = "2025-10-07 12:30:50 EDT"; + public static final int GIT_REVISION = 42; + public static final String GIT_SHA = "b0d78855eb9f11e406324939938314ebe2e43a1a"; + public static final String GIT_DATE = "2025-10-20 18:51:17 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-10-07 12:36:50 EDT"; - public static final long BUILD_UNIX_TIME = 1759855010411L; + public static final String BUILD_DATE = "2025-10-23 16:54:57 EDT"; + public static final long BUILD_UNIX_TIME = 1761252897619L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index c2156e11..8dd8d3c2 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -10,20 +10,17 @@ import static yams.mechanisms.SmartMechanism.gearbox; import static yams.mechanisms.SmartMechanism.gearing; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; - -import org.frc5010.common.arch.GenericSubsystem; - import com.thethriftybot.ThriftyNova; - import edu.wpi.first.math.controller.ArmFeedforward; import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj2.command.Command; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.config.ShooterConfig; import yams.mechanisms.velocity.Shooter; import yams.motorcontrollers.SmartMotorController; @@ -35,31 +32,35 @@ public class ShooterSubsystem extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(10); - private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( - Map.entry(0.0, 0.0), - Map.entry(0.5, 500.0), - Map.entry(1.0, 1000.0), - Map.entry(1.5, 1500.0)); + private InterpolatingDoubleTreeMap distanceToVelocityMap = + InterpolatingDoubleTreeMap.ofEntries( + Map.entry(0.0, 0.0), + Map.entry(0.5, 500.0), + Map.entry(1.0, 1000.0), + Map.entry(1.5, 1500.0)); - private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) - .withClosedLoopController( - 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) - .withGearing(gearing(gearbox(3, 4))) - .withIdleMode(MotorMode.COAST) - .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withClosedLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) - .withControlMode(ControlMode.CLOSED_LOOP); + private final SmartMotorControllerConfig motorConfig = + new SmartMotorControllerConfig(this) + .withClosedLoopController( + 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.COAST) + .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withClosedLoopRampRate(Seconds.of(0.25)) + .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) + .withControlMode(ControlMode.CLOSED_LOOP); - private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + private final SmartMotorController motorController = + new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); /** Creates a new Shooter. */ - private final ShooterConfig shooterConfig = new ShooterConfig(motorController) - .withDiameter(Inches.of(4)) - .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); + private final ShooterConfig shooterConfig = + new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); private final Shooter shooter = new Shooter(shooterConfig); @@ -74,20 +75,21 @@ public Command setSpeed(double speed) { public Command launchToDistance(DoubleSupplier distanceSupplier) { return shooter.setSpeed( - () -> RPM.of( - distanceToVelocityMap.get( - distanceSupplier.getAsDouble()))); - + () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); } - public Command spinAtSpeed(DoubleSupplier speedSupplier){ + + public Command spinAtSpeed(DoubleSupplier speedSupplier) { return shooter.setSpeed(RPM.of(speedSupplier.getAsDouble())); } -public Supplier getVelocity(){ - return () -> shooter.getSpeed(); -} -public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range){ - return shooter.isNear(expected, range); -} + + public Supplier getVelocity() { + return () -> shooter.getSpeed(); + } + + public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range) { + return shooter.isNear(expected, range); + } + @Override public void periodic() { shooter.updateTelemetry(); @@ -97,5 +99,4 @@ public void periodic() { public void simulationPeriodic() { shooter.simIterate(); } - } diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index a5800220..3c5b540d 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -4,14 +4,7 @@ package frc.robot.blackteam; -import org.frc5010.common.arch.GenericSubsystem; - -import com.thethriftybot.ThriftyNova; - - import static edu.wpi.first.units.Units.Amps; -import static edu.wpi.first.units.Units.DegreesPerSecond; -import static edu.wpi.first.units.Units.DegreesPerSecondPerSecond; import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; @@ -19,20 +12,10 @@ import static yams.mechanisms.SmartMechanism.gearbox; import static yams.mechanisms.SmartMechanism.gearing; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; - -import org.frc5010.common.arch.GenericSubsystem; - import com.thethriftybot.ThriftyNova; - -import edu.wpi.first.math.controller.ArmFeedforward; -import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj2.command.Command; +import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.config.ShooterConfig; import yams.mechanisms.velocity.Shooter; import yams.motorcontrollers.SmartMotorController; @@ -41,48 +24,45 @@ import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; import yams.motorcontrollers.local.NovaWrapper; -import edu.wpi.first.math.controller.ArmFeedforward; -import yams.motorcontrollers.SmartMotorControllerConfig; -import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; -import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; /** Add your docs here. */ public class feeder extends GenericSubsystem { - private final ThriftyNova motor = new ThriftyNova(11); - private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) - .withGearing(gearing(gearbox(3, 4))) - .withIdleMode(MotorMode.BRAKE) - .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withOpenLoopRampRate(Seconds.of(0.25)) - .withControlMode(ControlMode.OPEN_LOOP); + private final ThriftyNova motor = new ThriftyNova(11); + private final SmartMotorControllerConfig motorConfig = + new SmartMotorControllerConfig(this) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.BRAKE) + .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withOpenLoopRampRate(Seconds.of(0.25)) + .withControlMode(ControlMode.OPEN_LOOP); + + private final SmartMotorController motorController = + new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + + private final ShooterConfig feederConfig = + new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); + + private final Shooter feeder = new Shooter(feederConfig); - private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + public Command setSpeed(double speed) { + return feeder.set(speed); + } - private final ShooterConfig feederConfig = new ShooterConfig(motorController) - .withDiameter(Inches.of(4)) - .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); + public feeder() {} - private final Shooter feeder = new Shooter(feederConfig); - - public Command setSpeed(double speed) { - return feeder.set(speed); - } - public feeder(){ - - - } + @Override + public void periodic() { + feeder.updateTelemetry(); + } - @Override - public void periodic() { - feeder.updateTelemetry(); - } - @Override - public void simulationPeriodic() { - feeder.simIterate(); - } -} \ No newline at end of file + @Override + public void simulationPeriodic() { + feeder.simIterate(); + } +} diff --git a/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java b/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java index 789e57fc..e24eed50 100644 --- a/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java +++ b/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java @@ -301,7 +301,6 @@ public void initialize() { goalEndState = new GoalEndState(goalEndState.velocityMPS(), targetPose.getRotation()); } } - if (currentPose.getTranslation().getDistance(targetPose.getTranslation()) < 0.1) { output.accept(new ChassisSpeeds(), DriveFeedforwards.zeros(robotConfig.numModules)); finish = true; From d996709ebdd807d74935bfe4620a8346837aa7ae Mon Sep 17 00:00:00 2001 From: Curt Date: Sun, 26 Oct 2025 13:48:47 -0400 Subject: [PATCH 08/93] Reorder and clean up imports in example classes Reorganized import statements in ExampleRobot.java and ExampleSubsystem.java for improved readability and consistency. No functional changes were made. --- .../java/frc/robot/example/ExampleRobot.java | 9 +++-- .../frc/robot/example/ExampleSubsystem.java | 34 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/main/java/frc/robot/example/ExampleRobot.java b/src/main/java/frc/robot/example/ExampleRobot.java index ab7170cb..05a535b0 100644 --- a/src/main/java/frc/robot/example/ExampleRobot.java +++ b/src/main/java/frc/robot/example/ExampleRobot.java @@ -4,6 +4,10 @@ package frc.robot.example; +import static edu.wpi.first.units.Units.RPM; + +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.StateMachine; import org.frc5010.common.arch.StateMachine.State; @@ -13,10 +17,6 @@ import org.frc5010.common.motors.function.PercentControlMotor; import org.frc5010.common.sensors.Controller; -import static edu.wpi.first.units.Units.RPM; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; - /** This is an example robot class. */ public class ExampleRobot extends GenericRobot { SwerveConstants swerveConstants; @@ -39,7 +39,6 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.createXButton().whileTrue(exampleSubsystem.setDutyCycle(0.5)); driver.createYButton().onTrue(exampleSubsystem.sysIdShooter()); - State idle = stateMachine.addState("idle", Commands.idle()); stateMachine.setInitialState(idle); State prepping = stateMachine.addState("running", exampleSubsystem.setVelocity(RPM.of(3000))); diff --git a/src/main/java/frc/robot/example/ExampleSubsystem.java b/src/main/java/frc/robot/example/ExampleSubsystem.java index cf331bb1..a54c4f60 100644 --- a/src/main/java/frc/robot/example/ExampleSubsystem.java +++ b/src/main/java/frc/robot/example/ExampleSubsystem.java @@ -1,8 +1,23 @@ package frc.robot.example; +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.Inches; +import static edu.wpi.first.units.Units.Meters; +import static edu.wpi.first.units.Units.MetersPerSecond; + +import com.revrobotics.spark.SparkMax; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.Trigger; import java.util.function.DoubleSupplier; import java.util.function.Supplier; - import org.frc5010.common.arch.GenericSubsystem; import org.frc5010.common.constants.GenericPID; import org.frc5010.common.constants.MotorFeedFwdConstants; @@ -23,23 +38,6 @@ import org.ironmaple.simulation.seasonspecific.crescendo2024.NoteOnFly; import org.ironmaple.simulation.seasonspecific.reefscape2025.ReefscapeAlgaeOnFly; import org.littletonrobotics.junction.Logger; - -import com.revrobotics.spark.SparkMax; - -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.util.Units; -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.Meters; -import static edu.wpi.first.units.Units.MetersPerSecond; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; import yams.mechanisms.velocity.Shooter; public class ExampleSubsystem extends GenericSubsystem { From 5689da749d60b41af1dc63bc0d7892e269544731 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Mon, 27 Oct 2025 16:22:21 -0400 Subject: [PATCH 09/93] guragura jasdf;l;lkfh' --- simgui.json | 7 ++ src/main/java/frc/robot/BuildConstants.java | 10 +- .../frc/robot/blackteam/ShooterSubsystem.java | 85 ++++++++--------- src/main/java/frc/robot/blackteam/feeder.java | 92 ++++++++----------- 4 files changed, 91 insertions(+), 103 deletions(-) diff --git a/simgui.json b/simgui.json index c0095c86..674109fd 100644 --- a/simgui.json +++ b/simgui.json @@ -82,6 +82,7 @@ "/SmartDashboard/Field": "Field2d", "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/LiveTuning": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", @@ -89,6 +90,7 @@ "/SmartDashboard/Pigeon 2 [13]": "Gyro", "/SmartDashboard/Robot Visual": "Mechanism2d", "/SmartDashboard/SendableChooser[0]": "String Chooser", + "/SmartDashboard/ShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/Swerve Drive": "Alerts", "/SmartDashboard/Turret/mechanism": "Mechanism2d", "/SmartDashboard/VisionSystemSim-Vision/Sim Field": "Field2d", @@ -102,6 +104,11 @@ "visible": true } }, + "/Shuffleboard/BlackRobot/Auto Modes": { + "window": { + "visible": true + } + }, "/Shuffleboard/ExampleRobot/Auto Modes": { "window": { "visible": true diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index c9e4a4ae..c4f9fb82 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 34; - public static final String GIT_SHA = "6b88da14ad57857152383eb368d9ebffd7060202"; - public static final String GIT_DATE = "2025-10-07 12:30:50 EDT"; + public static final int GIT_REVISION = 42; + public static final String GIT_SHA = "b0d78855eb9f11e406324939938314ebe2e43a1a"; + public static final String GIT_DATE = "2025-10-20 18:51:17 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-10-07 12:36:50 EDT"; - public static final long BUILD_UNIX_TIME = 1759855010411L; + public static final String BUILD_DATE = "2025-10-22 17:49:00 EDT"; + public static final long BUILD_UNIX_TIME = 1761169740728L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index c2156e11..8dd8d3c2 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -10,20 +10,17 @@ import static yams.mechanisms.SmartMechanism.gearbox; import static yams.mechanisms.SmartMechanism.gearing; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; - -import org.frc5010.common.arch.GenericSubsystem; - import com.thethriftybot.ThriftyNova; - import edu.wpi.first.math.controller.ArmFeedforward; import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj2.command.Command; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.config.ShooterConfig; import yams.mechanisms.velocity.Shooter; import yams.motorcontrollers.SmartMotorController; @@ -35,31 +32,35 @@ public class ShooterSubsystem extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(10); - private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( - Map.entry(0.0, 0.0), - Map.entry(0.5, 500.0), - Map.entry(1.0, 1000.0), - Map.entry(1.5, 1500.0)); + private InterpolatingDoubleTreeMap distanceToVelocityMap = + InterpolatingDoubleTreeMap.ofEntries( + Map.entry(0.0, 0.0), + Map.entry(0.5, 500.0), + Map.entry(1.0, 1000.0), + Map.entry(1.5, 1500.0)); - private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) - .withClosedLoopController( - 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) - .withGearing(gearing(gearbox(3, 4))) - .withIdleMode(MotorMode.COAST) - .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withClosedLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) - .withControlMode(ControlMode.CLOSED_LOOP); + private final SmartMotorControllerConfig motorConfig = + new SmartMotorControllerConfig(this) + .withClosedLoopController( + 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.COAST) + .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withClosedLoopRampRate(Seconds.of(0.25)) + .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) + .withControlMode(ControlMode.CLOSED_LOOP); - private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + private final SmartMotorController motorController = + new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); /** Creates a new Shooter. */ - private final ShooterConfig shooterConfig = new ShooterConfig(motorController) - .withDiameter(Inches.of(4)) - .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); + private final ShooterConfig shooterConfig = + new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); private final Shooter shooter = new Shooter(shooterConfig); @@ -74,20 +75,21 @@ public Command setSpeed(double speed) { public Command launchToDistance(DoubleSupplier distanceSupplier) { return shooter.setSpeed( - () -> RPM.of( - distanceToVelocityMap.get( - distanceSupplier.getAsDouble()))); - + () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); } - public Command spinAtSpeed(DoubleSupplier speedSupplier){ + + public Command spinAtSpeed(DoubleSupplier speedSupplier) { return shooter.setSpeed(RPM.of(speedSupplier.getAsDouble())); } -public Supplier getVelocity(){ - return () -> shooter.getSpeed(); -} -public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range){ - return shooter.isNear(expected, range); -} + + public Supplier getVelocity() { + return () -> shooter.getSpeed(); + } + + public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range) { + return shooter.isNear(expected, range); + } + @Override public void periodic() { shooter.updateTelemetry(); @@ -97,5 +99,4 @@ public void periodic() { public void simulationPeriodic() { shooter.simIterate(); } - } diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index a5800220..3c5b540d 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -4,14 +4,7 @@ package frc.robot.blackteam; -import org.frc5010.common.arch.GenericSubsystem; - -import com.thethriftybot.ThriftyNova; - - import static edu.wpi.first.units.Units.Amps; -import static edu.wpi.first.units.Units.DegreesPerSecond; -import static edu.wpi.first.units.Units.DegreesPerSecondPerSecond; import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; @@ -19,20 +12,10 @@ import static yams.mechanisms.SmartMechanism.gearbox; import static yams.mechanisms.SmartMechanism.gearing; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; - -import org.frc5010.common.arch.GenericSubsystem; - import com.thethriftybot.ThriftyNova; - -import edu.wpi.first.math.controller.ArmFeedforward; -import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj2.command.Command; +import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.config.ShooterConfig; import yams.mechanisms.velocity.Shooter; import yams.motorcontrollers.SmartMotorController; @@ -41,48 +24,45 @@ import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; import yams.motorcontrollers.local.NovaWrapper; -import edu.wpi.first.math.controller.ArmFeedforward; -import yams.motorcontrollers.SmartMotorControllerConfig; -import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; -import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; /** Add your docs here. */ public class feeder extends GenericSubsystem { - private final ThriftyNova motor = new ThriftyNova(11); - private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) - .withGearing(gearing(gearbox(3, 4))) - .withIdleMode(MotorMode.BRAKE) - .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withOpenLoopRampRate(Seconds.of(0.25)) - .withControlMode(ControlMode.OPEN_LOOP); + private final ThriftyNova motor = new ThriftyNova(11); + private final SmartMotorControllerConfig motorConfig = + new SmartMotorControllerConfig(this) + .withGearing(gearing(gearbox(3, 4))) + .withIdleMode(MotorMode.BRAKE) + .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withOpenLoopRampRate(Seconds.of(0.25)) + .withControlMode(ControlMode.OPEN_LOOP); + + private final SmartMotorController motorController = + new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + + private final ShooterConfig feederConfig = + new ShooterConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withUpperSoftLimit(RPM.of(5000)); + + private final Shooter feeder = new Shooter(feederConfig); - private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + public Command setSpeed(double speed) { + return feeder.set(speed); + } - private final ShooterConfig feederConfig = new ShooterConfig(motorController) - .withDiameter(Inches.of(4)) - .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); + public feeder() {} - private final Shooter feeder = new Shooter(feederConfig); - - public Command setSpeed(double speed) { - return feeder.set(speed); - } - public feeder(){ - - - } + @Override + public void periodic() { + feeder.updateTelemetry(); + } - @Override - public void periodic() { - feeder.updateTelemetry(); - } - @Override - public void simulationPeriodic() { - feeder.simIterate(); - } -} \ No newline at end of file + @Override + public void simulationPeriodic() { + feeder.simIterate(); + } +} From fe15524b8f3065f41b1ddc980613553c267f4111 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 27 Oct 2025 17:29:42 -0400 Subject: [PATCH 10/93] New Paths --- .../deploy/pathplanner/autos/BlueToRedAuto.auto | 6 ++++++ .../deploy/pathplanner/autos/RedToBlueAuto.auto | 6 ++++++ src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 13 ++++++++++--- .../java/frc/robot/blackteam/ShooterSubsystem.java | 2 +- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main/deploy/pathplanner/autos/BlueToRedAuto.auto b/src/main/deploy/pathplanner/autos/BlueToRedAuto.auto index 93a63f8f..61765657 100644 --- a/src/main/deploy/pathplanner/autos/BlueToRedAuto.auto +++ b/src/main/deploy/pathplanner/autos/BlueToRedAuto.auto @@ -9,6 +9,12 @@ "data": { "pathName": "BlueToRed" } + }, + { + "type": "named", + "data": { + "name": "shoot" + } } ] } diff --git a/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto b/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto index e0f02868..6d7637eb 100644 --- a/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto +++ b/src/main/deploy/pathplanner/autos/RedToBlueAuto.auto @@ -9,6 +9,12 @@ "data": { "pathName": "RedToBlue" } + }, + { + "type": "named", + "data": { + "name": "shoot" + } } ] } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 94d2f41e..411dbaf1 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 42; - public static final String GIT_SHA = "b0d78855eb9f11e406324939938314ebe2e43a1a"; - public static final String GIT_DATE = "2025-10-20 18:51:17 EDT"; + public static final int GIT_REVISION = 48; + public static final String GIT_SHA = "d996709ebdd807d74935bfe4620a8346837aa7ae"; + public static final String GIT_DATE = "2025-10-26 13:48:47 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-10-23 16:54:57 EDT"; - public static final long BUILD_UNIX_TIME = 1761252897619L; + public static final String BUILD_DATE = "2025-10-27 16:16:52 EDT"; + public static final long BUILD_UNIX_TIME = 1761596212433L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 2d2980db..59aee332 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -1,5 +1,6 @@ package frc.robot.blackteam; +import com.pathplanner.lib.auto.NamedCommands; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import org.frc5010.common.arch.GenericRobot; @@ -15,9 +16,15 @@ public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); shooterSubsystem = new ShooterSubsystem(); - } - - @Override + NamedCommands.registerCommand("shoot", launchToDistance(20)); + } + + private Command launchToDistance(int i) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); + } + + @Override public void configureButtonBindings(Controller driver, Controller operator) { driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); } diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 8dd8d3c2..47dc12b2 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -99,4 +99,4 @@ public void periodic() { public void simulationPeriodic() { shooter.simIterate(); } -} +} \ No newline at end of file From 1a5c452a3ac0fda58260e162ed223cecde941fea Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Thu, 30 Oct 2025 18:58:19 -0400 Subject: [PATCH 11/93] State Machine Changes --- simgui.json | 28 ++++++---- src/main/deploy/black_robot/cameras.json | 4 +- .../deploy/black_robot/cameras/shooter.json | 14 +++-- .../deploy/pathplanner/paths/New Path.path | 54 +++++++++++++++++++ src/main/java/frc/robot/BuildConstants.java | 10 ++-- .../java/frc/robot/blackteam/BlackRobot.java | 16 +++--- .../frc/robot/blackteam/ShooterSubsystem.java | 2 +- .../camera/SimulatedFiducialTargetCamera.java | 10 ++-- .../common/subsystems/CameraSystem.java | 6 +-- 9 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 src/main/deploy/pathplanner/paths/New Path.path diff --git a/simgui.json b/simgui.json index 2200d680..0a834c3f 100644 --- a/simgui.json +++ b/simgui.json @@ -82,6 +82,7 @@ "/SmartDashboard/Field": "Field2d", "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", + "/SmartDashboard/Mechanisms/ShooterMech/FeederMotor/LiveTuning": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/LiveTuning": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", @@ -109,11 +110,6 @@ "visible": true } }, - "/SmartDashboard/Arm/mechanism": { - "window": { - "visible": true - } - }, "/SmartDashboard/DrivePoseEstimator/values/Pose Field": { "CARPET0": { "image": ".\\pictures\\carpet.png", @@ -707,6 +703,12 @@ "Pose": { "open": true }, + "Vision": { + "Camera shooter": { + "open": true + }, + "open": true + }, "open": true }, "SmartDashboard": { @@ -714,11 +716,13 @@ "DrivePoseEstimator": { "open": true }, - "open": true, "values": { "open": true } }, + "VisibleTargetSystem": { + "open": true + }, "open": true }, "Tuning": { @@ -734,13 +738,19 @@ "Turret": { "open": true } + }, + "photonvision": { + "intake": { + "open": true + }, + "open": true, + "shooter": { + "open": true + } } } }, "NetworkTables Info": { "visible": true - }, - "NetworkTables View": { - "visible": false } } diff --git a/src/main/deploy/black_robot/cameras.json b/src/main/deploy/black_robot/cameras.json index 1d32743e..0059afc4 100644 --- a/src/main/deploy/black_robot/cameras.json +++ b/src/main/deploy/black_robot/cameras.json @@ -1,9 +1,7 @@ { "cameras": [ - "intake.json", "localization.json", - "shooter.json", - "quest.json" + "shooter.json" ], "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" diff --git a/src/main/deploy/black_robot/cameras/shooter.json b/src/main/deploy/black_robot/cameras/shooter.json index b60f68bc..9cf3bb35 100644 --- a/src/main/deploy/black_robot/cameras/shooter.json +++ b/src/main/deploy/black_robot/cameras/shooter.json @@ -3,14 +3,18 @@ "use": "target", "type": "photonvision", "column": 0, - "x": -0.25, + "x": 0.25, "y": 0, "z": 0.25, "roll": 0, - "pitch": 20, - "yaw": 180, + "pitch": 10, + "yaw": 0, "targetFiducialIds": [ - 4, - 11 + 17, + 18, + 19, + 20, + 21, + 22 ] } diff --git a/src/main/deploy/pathplanner/paths/New Path.path b/src/main/deploy/pathplanner/paths/New Path.path new file mode 100644 index 00000000..51f0698e --- /dev/null +++ b/src/main/deploy/pathplanner/paths/New Path.path @@ -0,0 +1,54 @@ +{ + "version": "2025.0", + "waypoints": [ + { + "anchor": { + "x": 2.0, + "y": 7.0 + }, + "prevControl": null, + "nextControl": { + "x": 3.0, + "y": 7.0 + }, + "isLocked": false, + "linkedName": null + }, + { + "anchor": { + "x": 4.812601334742173, + "y": 1.232586014823413 + }, + "prevControl": { + "x": 3.391450364298724, + "y": 1.235462424252745 + }, + "nextControl": null, + "isLocked": false, + "linkedName": null + } + ], + "rotationTargets": [], + "constraintZones": [], + "pointTowardsZones": [], + "eventMarkers": [], + "globalConstraints": { + "maxVelocity": 5.25, + "maxAcceleration": 10.0, + "maxAngularVelocity": 540.0, + "maxAngularAcceleration": 720.0, + "nominalVoltage": 12.0, + "unlimited": false + }, + "goalEndState": { + "velocity": 0, + "rotation": 0.0 + }, + "reversed": false, + "folder": null, + "idealStartingState": { + "velocity": 0, + "rotation": 0.0 + }, + "useDefaultConstraints": true +} \ No newline at end of file diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 411dbaf1..2f08a52a 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 48; - public static final String GIT_SHA = "d996709ebdd807d74935bfe4620a8346837aa7ae"; - public static final String GIT_DATE = "2025-10-26 13:48:47 EDT"; + public static final int GIT_REVISION = 51; + public static final String GIT_SHA = "4abd8923167fdb794e6f9d97068f5504cc8b0bd3"; + public static final String GIT_DATE = "2025-10-29 11:13:08 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-10-27 16:16:52 EDT"; - public static final long BUILD_UNIX_TIME = 1761596212433L; + public static final String BUILD_DATE = "2025-10-30 17:58:49 EDT"; + public static final long BUILD_UNIX_TIME = 1761861529884L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 59aee332..0f4d170b 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -17,14 +17,14 @@ public BlackRobot(String directory) { drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); shooterSubsystem = new ShooterSubsystem(); NamedCommands.registerCommand("shoot", launchToDistance(20)); - } - - private Command launchToDistance(int i) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); - } - - @Override + } + + private Command launchToDistance(int i) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); + } + + @Override public void configureButtonBindings(Controller driver, Controller operator) { driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); } diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 47dc12b2..8dd8d3c2 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -99,4 +99,4 @@ public void periodic() { public void simulationPeriodic() { shooter.simIterate(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java b/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java index 2ca3c23b..89366727 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java @@ -4,17 +4,15 @@ package org.frc5010.common.sensors.camera; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; - -import org.photonvision.PhotonPoseEstimator.PoseStrategy; - import edu.wpi.first.apriltag.AprilTagFieldLayout; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Rotation3d; import edu.wpi.first.math.geometry.Transform3d; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import org.photonvision.PhotonPoseEstimator.PoseStrategy; /** A simulated camera using the PhotonVision library. */ public class SimulatedFiducialTargetCamera extends SimulatedCamera { diff --git a/src/main/java/org/frc5010/common/subsystems/CameraSystem.java b/src/main/java/org/frc5010/common/subsystems/CameraSystem.java index 21128052..2415ffe9 100644 --- a/src/main/java/org/frc5010/common/subsystems/CameraSystem.java +++ b/src/main/java/org/frc5010/common/subsystems/CameraSystem.java @@ -4,8 +4,9 @@ package org.frc5010.common.subsystems; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.wpilibj2.command.button.Trigger; import java.util.List; - import org.frc5010.common.arch.GenericSubsystem; import org.frc5010.common.constants.Constants; import org.frc5010.common.sensors.camera.GenericCamera; @@ -14,9 +15,6 @@ import org.photonvision.estimation.TargetModel; import org.photonvision.simulation.VisionTargetSim; -import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.wpilibj2.command.button.Trigger; - /** * This class is an abstract class that needs to be implemented by any subclass of CameraSystem. It * is responsible for updating the camera information. From 91b271ac39dfbf425ac8c66904050ca2894ac45f Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 3 Nov 2025 18:54:32 -0500 Subject: [PATCH 12/93] black team bot config --- src/main/deploy/black_robot/robot.json | 8 ++++---- .../black_robot/yagsl_swerve/modules/backleft.json | 14 +++++++------- .../yagsl_swerve/modules/backright.json | 14 +++++++------- .../yagsl_swerve/modules/frontleft.json | 14 +++++++------- .../yagsl_swerve/modules/frontright.json | 14 +++++++------- .../yagsl_swerve/modules/physicalproperties.json | 6 +++--- .../yagsl_swerve/modules/pidfproperties.json | 10 +++++----- .../black_robot/yagsl_swerve/swervedrive.json | 2 +- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/main/deploy/black_robot/robot.json b/src/main/deploy/black_robot/robot.json index 0ced2872..8c34455f 100644 --- a/src/main/deploy/black_robot/robot.json +++ b/src/main/deploy/black_robot/robot.json @@ -1,12 +1,12 @@ { "userConfig": "competition_mode.json", "driveType": "YAGSL_SWERVE_DRIVE", - "trackWidth": 16.8, + "trackWidth": 18.5625, "trackWidthUom": "in", - "wheelBase": 18.5, + "wheelBase": 18.5625, "wheelBaseUom": "in", - "wheelDiameter": 0.103386904, - "wheelDiameterUom": "m", + "wheelDiameter": 4, + "wheelDiameterUom": "in", "physicalMaxSpeed": 5.93, "physicalMaxSpeedUom": "m/s", "driveMotorGearRatio": 1.0, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 2ac2a1ab..8cf34515 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -1,16 +1,16 @@ { "drive": { - "type": "neo", - "id": 17, + "type": "nova_neo", + "id": 3, "canbus": null }, "angle": { - "type": "neo", - "id": 6, + "type": "nova_neo", + "id": 4, "canbus": null }, "encoder": { - "type": "canandcoder", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, @@ -20,7 +20,7 @@ }, "absoluteEncoderInverted": true, "location": { - "front": -8.37, - "left": 10.88 + "front": -9.2813, + "left": 9.2813 } } diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index eea317b8..dd2235d4 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -1,16 +1,16 @@ { "drive": { - "type": "neo", - "id": 18, + "type": "nova_neo", + "id": 5, "canbus": null }, "angle": { - "type": "neo", - "id": 2, + "type": "nova_neo", + "id": 4, "canbus": null }, "encoder": { - "type": "canandcoder", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, @@ -20,7 +20,7 @@ }, "absoluteEncoderInverted": true, "location": { - "front": -8.37, - "left": -10.88 + "front": -9.2813, + "left": -9.2813 } } diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index 1c03c3f1..5406ad95 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -1,16 +1,16 @@ { "drive": { - "type": "neo", - "id": 15, + "type": "nova_neo", + "id": 1, "canbus": null }, "angle": { - "type": "neo", - "id": 4, + "type": "nova_neo", + "id": 2, "canbus": null }, "encoder": { - "type": "canandcoder", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, @@ -20,7 +20,7 @@ }, "absoluteEncoderInverted": true, "location": { - "front": 8.37, - "left": 10.88 + "front": 9.2813, + "left": 9.2813 } } diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index 6218565f..068f6d64 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -1,16 +1,16 @@ { "drive": { - "type": "neo", - "id": 16, + "type": "nova_neo", + "id": 7, "canbus": null }, "angle": { - "type": "neo", - "id": 3, + "type": "nova_neo", + "id": 8, "canbus": null }, "encoder": { - "type": "canandcoder", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, @@ -20,7 +20,7 @@ }, "absoluteEncoderInverted": true, "location": { - "front": 8.37, - "left": -10.88 + "front": 9.2813, + "left": -9.2813 } } diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json index c921b000..66939aba 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/physicalproperties.json @@ -1,17 +1,17 @@ { "conversionFactors": { "angle": { - "gearRatio": 12.8, + "gearRatio": 25, "factor": 0 }, "drive": { - "gearRatio": 8.14, + "gearRatio": 6.75, "diameter": 4, "factor": 0 } }, "currentLimit": { - "drive": 100, + "drive": 40, "angle": 20 }, "rampRate": { diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json index 5924fdbb..e09a7131 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json @@ -1,16 +1,16 @@ { "drive": { - "p": 0.00023, - "i": 0.0000002, - "d": 1, + "p": 0.00001, + "i": 0, + "d": 0, "f": 0, "iz": 0 }, "angle": { - "p": 0.0020645, + "p": 0.0005, "i": 0, "d": 0, - "f": 0.001, + "f": 0, "iz": 0 } } diff --git a/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json b/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json index f4876233..993fe290 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json +++ b/src/main/deploy/black_robot/yagsl_swerve/swervedrive.json @@ -1,7 +1,7 @@ { "imu": { "type": "pigeon2", - "id": 13, + "id": 9, "canbus": "" }, "invertedIMU": false, From e4afe64a3797b8a9612abdf516d50b74cfb371cb Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Wed, 5 Nov 2025 18:56:06 -0500 Subject: [PATCH 13/93] Working changes --- src/main/deploy/black_robot/cameras.json | 5 +---- src/main/deploy/black_robot/yagsl_drivetrain.json | 2 +- .../black_robot/yagsl_swerve/modules/backleft.json | 2 +- .../black_robot/yagsl_swerve/modules/backright.json | 4 ++-- .../black_robot/yagsl_swerve/modules/frontleft.json | 2 +- .../black_robot/yagsl_swerve/modules/frontright.json | 2 +- src/main/deploy/robots.json | 12 ++++++------ src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 3 +-- 9 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/deploy/black_robot/cameras.json b/src/main/deploy/black_robot/cameras.json index 0059afc4..f308fd3e 100644 --- a/src/main/deploy/black_robot/cameras.json +++ b/src/main/deploy/black_robot/cameras.json @@ -1,8 +1,5 @@ { - "cameras": [ - "localization.json", - "shooter.json" - ], + "cameras": [], "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" } diff --git a/src/main/deploy/black_robot/yagsl_drivetrain.json b/src/main/deploy/black_robot/yagsl_drivetrain.json index c6beb5d7..d99f723b 100644 --- a/src/main/deploy/black_robot/yagsl_drivetrain.json +++ b/src/main/deploy/black_robot/yagsl_drivetrain.json @@ -1,5 +1,5 @@ { - "directory": "basic_robot/yagsl_swerve", + "directory": "black_robot/yagsl_swerve", "turningMotorGearRatio": 1.0, "driveModules": [ "frontleft.json", diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 8cf34515..3a208ec1 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "thrifty_nova", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index dd2235d4..7142bf0e 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -6,11 +6,11 @@ }, "angle": { "type": "nova_neo", - "id": 4, + "id": 6, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "thrifty_nova", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index 5406ad95..ae2d64aa 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "thrifty_nova", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index 068f6d64..683a853c 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "thrifty_nova", "id": 0, "canbus": null }, diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 7121e5e3..9657d602 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -1,6 +1,12 @@ { "competitionPin": 0, "robots": { + "black_robot": { + "id": "black_robot", + "robotClass": "frc.robot.blackteam.BlackRobot", + "simulate": true, + "competition": true + }, "basic_robot": { "id": "basic_robot", "robotClass": "frc.robot.example.ExampleRobot", @@ -10,12 +16,6 @@ "id": "baby_swerve", "robotClass": "frc.robot.baby_swerve.BabySwerve", "simulate": false - }, - "black_robot": { - "id": "black_robot", - "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": true, - "competition": true } } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 2f08a52a..87e79723 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 51; - public static final String GIT_SHA = "4abd8923167fdb794e6f9d97068f5504cc8b0bd3"; - public static final String GIT_DATE = "2025-10-29 11:13:08 EDT"; + public static final int GIT_REVISION = 57; + public static final String GIT_SHA = "91b271ac39dfbf425ac8c66904050ca2894ac45f"; + public static final String GIT_DATE = "2025-11-03 18:54:32 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-10-30 17:58:49 EDT"; - public static final long BUILD_UNIX_TIME = 1761861529884L; + public static final String BUILD_DATE = "2025-11-05 18:40:00 EST"; + public static final long BUILD_UNIX_TIME = 1762386000007L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 0f4d170b..58f9fc01 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -1,6 +1,5 @@ package frc.robot.blackteam; -import com.pathplanner.lib.auto.NamedCommands; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import org.frc5010.common.arch.GenericRobot; @@ -16,7 +15,7 @@ public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); shooterSubsystem = new ShooterSubsystem(); - NamedCommands.registerCommand("shoot", launchToDistance(20)); + // NamedCommands.registerCommand("shoot", launchToDistance(20)); } private Command launchToDistance(int i) { From 737c05c232156879a122cd1d03fb18e4a2062fd9 Mon Sep 17 00:00:00 2001 From: Curt Date: Thu, 6 Nov 2025 12:35:43 -0500 Subject: [PATCH 14/93] Use copied YAGSL --- simgui.json | 1 + .../yagsl_swerve/modules/backleft.json | 2 +- .../yagsl_swerve/modules/backright.json | 2 +- .../yagsl_swerve/modules/frontleft.json | 2 +- .../yagsl_swerve/modules/frontright.json | 2 +- src/main/java/frc/robot/BuildConstants.java | 10 +- src/main/java/swervelib/SwerveController.java | 223 +++ src/main/java/swervelib/SwerveDrive.java | 1456 +++++++++++++++++ src/main/java/swervelib/SwerveDriveTest.java | 477 ++++++ .../java/swervelib/SwerveInputStream.java | 914 +++++++++++ src/main/java/swervelib/SwerveModule.java | 890 ++++++++++ .../encoders/AnalogAbsoluteEncoderSwerve.java | 126 ++ .../swervelib/encoders/CANCoderSwerve.java | 211 +++ .../swervelib/encoders/CanAndMagSwerve.java | 97 ++ .../encoders/DIODutyCycleEncoderSwerve.java | 106 ++ .../encoders/SparkFlexEncoderSwerve.java | 136 ++ .../encoders/SparkMaxAnalogEncoderSwerve.java | 185 +++ .../encoders/SparkMaxEncoderSwerve.java | 190 +++ .../encoders/SwerveAbsoluteEncoder.java | 64 + .../encoders/TalonSRXEncoderSwerve.java | 82 + .../encoders/ThriftyNovaEncoderSwerve.java | 105 ++ .../java/swervelib/encoders/package-info.java | 5 + .../java/swervelib/imu/ADIS16448Swerve.java | 120 ++ .../java/swervelib/imu/ADIS16470Swerve.java | 121 ++ .../java/swervelib/imu/ADXRS450Swerve.java | 115 ++ .../java/swervelib/imu/AnalogGyroSwerve.java | 120 ++ .../java/swervelib/imu/CanandgyroSwerve.java | 117 ++ src/main/java/swervelib/imu/NavXSwerve.java | 137 ++ .../java/swervelib/imu/Pigeon2Swerve.java | 151 ++ src/main/java/swervelib/imu/PigeonSwerve.java | 121 ++ .../imu/PigeonViaTalonSRXSwerve.java | 130 ++ src/main/java/swervelib/imu/SwerveIMU.java | 69 + src/main/java/swervelib/imu/package-info.java | 2 + src/main/java/swervelib/math/Matter.java | 32 + src/main/java/swervelib/math/SwerveMath.java | 394 +++++ .../java/swervelib/math/package-info.java | 6 + .../swervelib/motors/SparkFlexSwerve.java | 455 ++++++ .../motors/SparkMaxBrushedMotorSwerve.java | 555 +++++++ .../java/swervelib/motors/SparkMaxSwerve.java | 456 ++++++ .../java/swervelib/motors/SwerveMotor.java | 195 +++ .../java/swervelib/motors/TalonFXSSwerve.java | 399 +++++ .../java/swervelib/motors/TalonFXSwerve.java | 402 +++++ .../java/swervelib/motors/TalonSRXSwerve.java | 440 +++++ .../swervelib/motors/ThriftyNovaSwerve.java | 471 ++++++ .../java/swervelib/motors/package-info.java | 2 + src/main/java/swervelib/package-info.java | 6 + src/main/java/swervelib/parser/Cache.java | 87 + .../java/swervelib/parser/PIDFConfig.java | 88 + .../parser/SwerveControllerConfiguration.java | 57 + .../parser/SwerveDriveConfiguration.java | 139 ++ .../parser/SwerveModuleConfiguration.java | 141 ++ .../SwerveModulePhysicalCharacteristics.java | 112 ++ .../java/swervelib/parser/SwerveParser.java | 180 ++ .../parser/deserializer/PIDFRange.java | 10 + .../deserializer/ReflectionsManager.java | 82 + .../parser/deserializer/package-info.java | 2 + .../parser/json/ControllerPropertiesJson.java | 30 + .../swervelib/parser/json/DeviceJson.java | 301 ++++ .../swervelib/parser/json/ModuleJson.java | 122 ++ .../parser/json/MotorConfigDouble.java | 24 + .../swervelib/parser/json/MotorConfigInt.java | 24 + .../parser/json/PIDFPropertiesJson.java | 12 + .../parser/json/PhysicalPropertiesJson.java | 75 + .../parser/json/SwerveDriveJson.java | 15 + .../modules/AngleConversionFactorsJson.java | 27 + .../parser/json/modules/BoolMotorJson.java | 10 + .../json/modules/ConversionFactorsJson.java | 40 + .../modules/DriveConversionFactorsJson.java | 29 + .../parser/json/modules/LocationJson.java | 14 + .../parser/json/modules/package-info.java | 2 + .../swervelib/parser/json/package-info.java | 2 + .../java/swervelib/parser/package-info.java | 2 + .../simulation/SwerveIMUSimulation.java | 98 ++ .../simulation/SwerveModuleSimulation.java | 91 ++ .../swervelib/simulation/package-info.java | 2 + .../telemetry/SwerveDriveTelemetry.java | 346 ++++ .../swervelib/telemetry/package-info.java | 2 + vendordeps/yagsl-2025.8.0.json | 64 - 78 files changed, 12459 insertions(+), 73 deletions(-) create mode 100644 src/main/java/swervelib/SwerveController.java create mode 100644 src/main/java/swervelib/SwerveDrive.java create mode 100644 src/main/java/swervelib/SwerveDriveTest.java create mode 100644 src/main/java/swervelib/SwerveInputStream.java create mode 100644 src/main/java/swervelib/SwerveModule.java create mode 100644 src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/CANCoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/CanAndMagSwerve.java create mode 100644 src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java create mode 100644 src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java create mode 100644 src/main/java/swervelib/encoders/package-info.java create mode 100644 src/main/java/swervelib/imu/ADIS16448Swerve.java create mode 100644 src/main/java/swervelib/imu/ADIS16470Swerve.java create mode 100644 src/main/java/swervelib/imu/ADXRS450Swerve.java create mode 100644 src/main/java/swervelib/imu/AnalogGyroSwerve.java create mode 100644 src/main/java/swervelib/imu/CanandgyroSwerve.java create mode 100644 src/main/java/swervelib/imu/NavXSwerve.java create mode 100644 src/main/java/swervelib/imu/Pigeon2Swerve.java create mode 100644 src/main/java/swervelib/imu/PigeonSwerve.java create mode 100644 src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java create mode 100644 src/main/java/swervelib/imu/SwerveIMU.java create mode 100644 src/main/java/swervelib/imu/package-info.java create mode 100644 src/main/java/swervelib/math/Matter.java create mode 100644 src/main/java/swervelib/math/SwerveMath.java create mode 100644 src/main/java/swervelib/math/package-info.java create mode 100644 src/main/java/swervelib/motors/SparkFlexSwerve.java create mode 100644 src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java create mode 100644 src/main/java/swervelib/motors/SparkMaxSwerve.java create mode 100644 src/main/java/swervelib/motors/SwerveMotor.java create mode 100644 src/main/java/swervelib/motors/TalonFXSSwerve.java create mode 100644 src/main/java/swervelib/motors/TalonFXSwerve.java create mode 100644 src/main/java/swervelib/motors/TalonSRXSwerve.java create mode 100644 src/main/java/swervelib/motors/ThriftyNovaSwerve.java create mode 100644 src/main/java/swervelib/motors/package-info.java create mode 100644 src/main/java/swervelib/package-info.java create mode 100644 src/main/java/swervelib/parser/Cache.java create mode 100644 src/main/java/swervelib/parser/PIDFConfig.java create mode 100644 src/main/java/swervelib/parser/SwerveControllerConfiguration.java create mode 100644 src/main/java/swervelib/parser/SwerveDriveConfiguration.java create mode 100644 src/main/java/swervelib/parser/SwerveModuleConfiguration.java create mode 100644 src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java create mode 100644 src/main/java/swervelib/parser/SwerveParser.java create mode 100644 src/main/java/swervelib/parser/deserializer/PIDFRange.java create mode 100644 src/main/java/swervelib/parser/deserializer/ReflectionsManager.java create mode 100644 src/main/java/swervelib/parser/deserializer/package-info.java create mode 100644 src/main/java/swervelib/parser/json/ControllerPropertiesJson.java create mode 100644 src/main/java/swervelib/parser/json/DeviceJson.java create mode 100644 src/main/java/swervelib/parser/json/ModuleJson.java create mode 100644 src/main/java/swervelib/parser/json/MotorConfigDouble.java create mode 100644 src/main/java/swervelib/parser/json/MotorConfigInt.java create mode 100644 src/main/java/swervelib/parser/json/PIDFPropertiesJson.java create mode 100644 src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java create mode 100644 src/main/java/swervelib/parser/json/SwerveDriveJson.java create mode 100644 src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java create mode 100644 src/main/java/swervelib/parser/json/modules/BoolMotorJson.java create mode 100644 src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java create mode 100644 src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java create mode 100644 src/main/java/swervelib/parser/json/modules/LocationJson.java create mode 100644 src/main/java/swervelib/parser/json/modules/package-info.java create mode 100644 src/main/java/swervelib/parser/json/package-info.java create mode 100644 src/main/java/swervelib/parser/package-info.java create mode 100644 src/main/java/swervelib/simulation/SwerveIMUSimulation.java create mode 100644 src/main/java/swervelib/simulation/SwerveModuleSimulation.java create mode 100644 src/main/java/swervelib/simulation/package-info.java create mode 100644 src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java create mode 100644 src/main/java/swervelib/telemetry/package-info.java delete mode 100644 vendordeps/yagsl-2025.8.0.json diff --git a/simgui.json b/simgui.json index 0a834c3f..e6e541d2 100644 --- a/simgui.json +++ b/simgui.json @@ -88,6 +88,7 @@ "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", "/SmartDashboard/Pigeon 2 (v6) [13]": "Gyro", + "/SmartDashboard/Pigeon 2 (v6) [9]": "Gyro", "/SmartDashboard/Pigeon 2 [13]": "Gyro", "/SmartDashboard/Robot Visual": "Mechanism2d", "/SmartDashboard/SendableChooser[0]": "String Chooser", diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 3a208ec1..8cf34515 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index 7142bf0e..750a2c7b 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index ae2d64aa..5406ad95 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index 683a853c..068f6d64 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -10,7 +10,7 @@ "canbus": null }, "encoder": { - "type": "thrifty_nova", + "type": "thrifty_nova_redux", "id": 0, "canbus": null }, diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 87e79723..1f25c6bb 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 57; - public static final String GIT_SHA = "91b271ac39dfbf425ac8c66904050ca2894ac45f"; - public static final String GIT_DATE = "2025-11-03 18:54:32 EST"; + public static final int GIT_REVISION = 58; + public static final String GIT_SHA = "e4afe64a3797b8a9612abdf516d50b74cfb371cb"; + public static final String GIT_DATE = "2025-11-05 18:56:06 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-05 18:40:00 EST"; - public static final long BUILD_UNIX_TIME = 1762386000007L; + public static final String BUILD_DATE = "2025-11-06 12:33:11 EST"; + public static final long BUILD_UNIX_TIME = 1762450391608L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/swervelib/SwerveController.java b/src/main/java/swervelib/SwerveController.java new file mode 100644 index 00000000..809ade58 --- /dev/null +++ b/src/main/java/swervelib/SwerveController.java @@ -0,0 +1,223 @@ +package swervelib; + +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.filter.SlewRateLimiter; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import swervelib.parser.SwerveControllerConfiguration; + +/** Controller class used to convert raw inputs into robot speeds. */ +public class SwerveController { + + /** + * {@link SwerveControllerConfiguration} object storing data to generate the {@link PIDController} + * for controlling the robot heading, and deadband for heading joystick. + */ + public final SwerveControllerConfiguration config; + /** PID Controller for the robot heading. */ + public final PIDController thetaController; // TODO: Switch to ProfilePIDController + /** Last angle as a scalar [-1,1] the robot was set to. */ + public double lastAngleScalar; + /** {@link SlewRateLimiter} for movement in the X direction in meters/second. */ + public SlewRateLimiter xLimiter = null; + /** {@link SlewRateLimiter} for movement in the Y direction in meters/second. */ + public SlewRateLimiter yLimiter = null; + /** {@link SlewRateLimiter} for angular movement in radians/second. */ + public SlewRateLimiter angleLimiter = null; + + /** + * Construct the SwerveController object which is used for determining the speeds of the robot + * based on controller input. + * + * @param cfg {@link SwerveControllerConfiguration} containing the PIDF variables for the heading + * PIDF. + */ + public SwerveController(SwerveControllerConfiguration cfg) { + config = cfg; + thetaController = config.headingPIDF.createPIDController(); + thetaController.enableContinuousInput(-Math.PI, Math.PI); + lastAngleScalar = 0; + } + + /** + * Helper function to get the {@link Translation2d} of the chassis speeds given the {@link + * ChassisSpeeds}. + * + * @param speeds Chassis speeds. + * @return {@link Translation2d} of the speed the robot is going in. + */ + public static Translation2d getTranslation2d(ChassisSpeeds speeds) { + return new Translation2d(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond); + } + + /** + * Add slew rate limiters to all controls. This prevents the robot from ramping up too much. To + * disable a {@link SlewRateLimiter} set the desired one to null. + * + * @param x The {@link SlewRateLimiter} for the X velocity in meters/second. + * @param y The {@link SlewRateLimiter} for the Y velocity in meters/second. + * @param angle The {@link SlewRateLimiter} for the angular velocity in radians/second. + */ + public void addSlewRateLimiters(SlewRateLimiter x, SlewRateLimiter y, SlewRateLimiter angle) { + xLimiter = x; + yLimiter = y; + angleLimiter = angle; + } + + /** + * Calculate the hypot deadband and check if the joystick is within it. + * + * @param x The x value for the joystick in which the deadband should be applied. + * @param y The y value for the joystick in which the deadband should be applied. + * @return Whether the values are within the deadband from {@link + * SwerveControllerConfiguration#angleJoyStickRadiusDeadband}. + */ + public boolean withinHypotDeadband(double x, double y) { + return Math.hypot(x, y) < config.angleJoyStickRadiusDeadband; + } + + /** + * Get the chassis speeds based on controller input of 1 joystick [-1,1] and an angle. + * + * @param xInput X joystick input for the robot to move in the X direction. X = xInput * maxSpeed + * @param yInput Y joystick input for the robot to move in the Y direction. Y = yInput * maxSpeed; + * @param angle The desired angle of the robot in radians. + * @param currentHeadingAngleRadians The current robot heading in radians. + * @param maxSpeed Maximum speed in meters per second. + * @return {@link ChassisSpeeds} which can be sent to the Swerve Drive. + */ + public ChassisSpeeds getTargetSpeeds( + double xInput, + double yInput, + double angle, + double currentHeadingAngleRadians, + double maxSpeed) { + // Convert joystick inputs to m/s by scaling by max linear speed. Also uses a cubic function + // to allow for precise control and fast movement. + double x = xInput * maxSpeed; + double y = yInput * maxSpeed; + + return getRawTargetSpeeds(x, y, angle, currentHeadingAngleRadians); + } + + /** + * Get the angle in radians based off of the heading joysticks. + * + * @param headingX X joystick which controls the angle of the robot. + * @param headingY Y joystick which controls the angle of the robot. + * @return angle in radians from the joystick. + */ + public double getJoystickAngle(double headingX, double headingY) { + lastAngleScalar = + withinHypotDeadband(headingX, headingY) ? lastAngleScalar : Math.atan2(headingX, headingY); + return lastAngleScalar; + } + + /** + * Get the chassis speeds based on controller input of 2 joysticks. One for speeds in which + * direction. The other for the angle of the robot. + * + * @param xInput X joystick input for the robot to move in the X direction. + * @param yInput Y joystick input for the robot to move in the Y direction. + * @param headingX X joystick which controls the angle of the robot. + * @param headingY Y joystick which controls the angle of the robot. + * @param currentHeadingAngleRadians The current robot heading in radians. + * @param maxSpeed Maximum speed of the drive motors in meters per second, multiplier of the + * xInput and yInput. + * @return {@link ChassisSpeeds} which can be sent to the Swerve Drive. + */ + public ChassisSpeeds getTargetSpeeds( + double xInput, + double yInput, + double headingX, + double headingY, + double currentHeadingAngleRadians, + double maxSpeed) { + // Converts the horizontal and vertical components to the commanded angle, in radians, unless + // the joystick is near + // the center (i. e. has been released), in which case the angle is held at the last valid + // joystick input (hold + // position when stick released). + double angle = + withinHypotDeadband(headingX, headingY) ? lastAngleScalar : Math.atan2(headingX, headingY); + ChassisSpeeds speeds = + getTargetSpeeds(xInput, yInput, angle, currentHeadingAngleRadians, maxSpeed); + + // Used for the position hold feature + lastAngleScalar = angle; + + return speeds; + } + + /** + * Get the {@link ChassisSpeeds} based of raw speeds desired in meters/second and heading in + * radians. + * + * @param xSpeed X speed in meters per second. + * @param ySpeed Y speed in meters per second. + * @param omega Angular velocity in radians/second. + * @return {@link ChassisSpeeds} the robot should move to. + */ + public ChassisSpeeds getRawTargetSpeeds(double xSpeed, double ySpeed, double omega) { + if (xLimiter != null) { + xSpeed = xLimiter.calculate(xSpeed); + } + if (yLimiter != null) { + ySpeed = yLimiter.calculate(ySpeed); + } + if (angleLimiter != null) { + omega = angleLimiter.calculate(omega); + } + + return new ChassisSpeeds(xSpeed, ySpeed, omega); + } + + /** + * Get the {@link ChassisSpeeds} based of raw speeds desired in meters/second and heading in + * radians. + * + * @param xSpeed X speed in meters per second. + * @param ySpeed Y speed in meters per second. + * @param targetHeadingAngleRadians Target heading in radians. + * @param currentHeadingAngleRadians Current heading in radians. + * @return {@link ChassisSpeeds} the robot should move to. + */ + public ChassisSpeeds getRawTargetSpeeds( + double xSpeed, + double ySpeed, + double targetHeadingAngleRadians, + double currentHeadingAngleRadians) { + // Calculates an angular rate using a PIDController and the commanded angle. Returns a value + // between -1 and 1 + // which is then scaled to be between -maxAngularVelocity and +maxAngularVelocity. + return getRawTargetSpeeds( + xSpeed, + ySpeed, + thetaController.calculate(currentHeadingAngleRadians, targetHeadingAngleRadians) + * config.maxAngularVelocity); + } + + /** + * Calculate the angular velocity given the current and target heading angle in radians. + * + * @param currentHeadingAngleRadians The current heading of the robot in radians. + * @param targetHeadingAngleRadians The target heading of the robot in radians. + * @return Angular velocity in radians per second. + */ + public double headingCalculate( + double currentHeadingAngleRadians, double targetHeadingAngleRadians) { + return thetaController.calculate(currentHeadingAngleRadians, targetHeadingAngleRadians) + * config.maxAngularVelocity; + } + + /** + * Set a new maximum angular velocity that is different from the auto-generated one. Modified the + * {@link SwerveControllerConfiguration#maxAngularVelocity} field which is used in the {@link + * SwerveController} class for {@link ChassisSpeeds} generation. + * + * @param angularVelocity Angular velocity in radians per second. + */ + public void setMaximumChassisAngularVelocity(double angularVelocity) { + config.maxAngularVelocity = angularVelocity; + } +} diff --git a/src/main/java/swervelib/SwerveDrive.java b/src/main/java/swervelib/SwerveDrive.java new file mode 100644 index 00000000..6354afe1 --- /dev/null +++ b/src/main/java/swervelib/SwerveDrive.java @@ -0,0 +1,1456 @@ +package swervelib; + +import static edu.wpi.first.hal.FRCNetComm.tInstances.kRobotDriveSwerve_YAGSL; +import static edu.wpi.first.hal.FRCNetComm.tResourceType.kResourceType_RobotDrive; +import static edu.wpi.first.units.Units.Inches; +import static edu.wpi.first.units.Units.KilogramSquareMeters; +import static edu.wpi.first.units.Units.Kilograms; +import static edu.wpi.first.units.Units.Meters; +import static edu.wpi.first.units.Units.Newtons; +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.units.Units.Volts; + +import edu.wpi.first.hal.HAL; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.controller.SimpleMotorFeedforward; +import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; +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.Rotation3d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.math.trajectory.Trajectory; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Force; +import edu.wpi.first.units.measure.LinearVelocity; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Notifier; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.smartdashboard.Field2d; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.ironmaple.simulation.SimulatedArena; +import org.ironmaple.simulation.drivesims.AbstractDriveTrainSimulation; +import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; +import org.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; +import org.ironmaple.simulation.drivesims.configs.SwerveModuleSimulationConfig; +import swervelib.encoders.CANCoderSwerve; +import swervelib.imu.Pigeon2Swerve; +import swervelib.imu.SwerveIMU; +import swervelib.math.SwerveMath; +import swervelib.motors.TalonFXSwerve; +import swervelib.parser.Cache; +import swervelib.parser.SwerveControllerConfiguration; +import swervelib.parser.SwerveDriveConfiguration; +import swervelib.simulation.SwerveIMUSimulation; +import swervelib.telemetry.SwerveDriveTelemetry; +import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; + +/** Swerve Drive class representing and controlling the swerve drive. */ +public class SwerveDrive implements AutoCloseable { + + /** Swerve Kinematics object. */ + public final SwerveDriveKinematics kinematics; + /** Swerve drive configuration. */ + public final SwerveDriveConfiguration swerveDriveConfiguration; + /** Swerve odometry. */ + public final SwerveDrivePoseEstimator swerveDrivePoseEstimator; + /** IMU reading cache for robot readings. */ + public final Cache imuReadingCache; + /** Swerve modules. */ + private final SwerveModule[] swerveModules; + /** WPILib {@link Notifier} to keep odometry up to date. */ + private final Notifier odometryThread; + /** Odometry lock to ensure thread safety. */ + private final Lock odometryLock = new ReentrantLock(); + /** Alert to recommend Tuner X if the configuration is compatible. */ + private final Alert tunerXRecommendation = + new Alert( + "Swerve Drive", + "Your Swerve Drive is compatible with Tuner X swerve generator, please consider using that instead of YAGSL. More information here!\n" + + "https://pro.docs.ctr-electronics.com/en/latest/docs/tuner/tuner-swerve/index.html", + AlertType.kWarning); + /** NT4 Publisher for the IMU reading. */ + private final DoublePublisher rawIMUPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/imu/raw") + .publish(); + /** NT4 Publisher for the IMU reading adjusted by offset and inversion. */ + private final DoublePublisher adjustedIMUPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/imu/adjusted") + .publish(); + /** Field object. */ + public Field2d field = new Field2d(); + /** Swerve controller for controlling heading of the robot. */ + public SwerveController swerveController; + /** + * Correct chassis velocity in {@link SwerveDrive#drive(Translation2d, double, boolean, boolean)} + * using 254's correction. + */ + public boolean chassisVelocityCorrection = true; + /** + * Correct chassis velocity in {@link SwerveDrive#setChassisSpeeds(ChassisSpeeds chassisSpeeds)} + * (auto) using 254's correction during auto. + */ + public boolean autonomousChassisVelocityCorrection = false; + /** + * Correct for skew that scales with angular velocity in {@link SwerveDrive#drive(Translation2d, + * double, boolean, boolean)} + */ + public boolean angularVelocityCorrection = false; + /** + * Correct for skew that scales with angular velocity in {@link + * SwerveDrive#setChassisSpeeds(ChassisSpeeds chassisSpeeds)} during auto. + */ + public boolean autonomousAngularVelocityCorrection = false; + /** Angular Velocity Correction Coefficent (expected values between -0.15 and 0.15). */ + public double angularVelocityCoefficient = 0; + /** Whether to correct heading when driving translationally. Set to true to enable. */ + public boolean headingCorrection = false; + /** MapleSim SwerveDrive. */ + private SwerveDriveSimulation mapleSimDrive; + /** Amount of seconds the duration of the timestep the speeds should be applied for. */ + private double discretizationdtSeconds = 0.02; + /** Deadband for speeds in heading correction. */ + private double HEADING_CORRECTION_DEADBAND = 0.01; + /** Swerve IMU device for sensing the heading of the robot. */ + private SwerveIMU imu; + /** Simulation of the swerve drive. */ + private SwerveIMUSimulation simIMU; + /** Counter to synchronize the modules relative encoder with absolute encoder when not moving. */ + private int moduleSynchronizationCounter = 0; + /** The last heading set in radians. */ + private double lastHeadingRadians = 0; + /** The absolute max speed that your robot can reach while translating in meters per second. */ + private double attainableMaxTranslationalSpeedMetersPerSecond = 0; + /** The absolute max speed the robot can reach while rotating radians per second. */ + private double attainableMaxRotationalVelocityRadiansPerSecond = 0; + /** Maximum speed of the robot in meters per second. */ + private double maxChassisSpeedMPS; + + /** + * Creates a new swerve drivebase subsystem. Robot is controlled via the {@link SwerveDrive#drive} + * method, or via the {@link SwerveDrive#setRawModuleStates} method. The {@link SwerveDrive#drive} + * method incorporates kinematics-- it takes a translation and rotation, as well as parameters for + * field-centric and closed-loop velocity control. {@link SwerveDrive#setRawModuleStates} takes a + * list of SwerveModuleStates and directly passes them to the modules. This subsystem also handles + * odometry. + * + * @param config The {@link SwerveDriveConfiguration} configuration to base the swerve drive off + * of. + * @param controllerConfig The {@link SwerveControllerConfiguration} to use when creating the + * {@link SwerveController}. + * @param maxSpeedMPS Maximum speed of the robot in meters per second, remember to use {@link + * Units#feetToMeters(double)} if you have feet per second! + * @param startingPose Starting {@link Pose2d} on the field. + */ + public SwerveDrive( + SwerveDriveConfiguration config, + SwerveControllerConfiguration controllerConfig, + double maxSpeedMPS, + Pose2d startingPose) { + this.attainableMaxTranslationalSpeedMetersPerSecond = this.maxChassisSpeedMPS = maxSpeedMPS; + this.attainableMaxRotationalVelocityRadiansPerSecond = + Math.PI * 2; // Defaulting to something reasonable for most robots + swerveDriveConfiguration = config; + swerveController = new SwerveController(controllerConfig); + // Create Kinematics from swerve module locations. + kinematics = new SwerveDriveKinematics(config.moduleLocationsMeters); + odometryThread = new Notifier(this::updateOdometry); + + this.swerveModules = config.modules; + + // Create an integrator for angle if the robot is being simulated to emulate an IMU + // If the robot is real, instantiate the IMU instead. + if (SwerveDriveTelemetry.isSimulation) { + DriveTrainSimulationConfig simulationConfig = + DriveTrainSimulationConfig.Default() + .withBumperSize( + Meters.of(config.getTracklength()).plus(Inches.of(5)), + Meters.of(config.getTrackwidth()).plus(Inches.of(5))) + .withRobotMass(Kilograms.of(config.physicalCharacteristics.robotMassKg)) + .withCustomModuleTranslations(config.moduleLocationsMeters) + .withGyro(config.getGyroSim()) + .withSwerveModule( + new SwerveModuleSimulationConfig( + config.getDriveMotorSim(), + config.getAngleMotorSim(), + config.physicalCharacteristics.conversionFactor.drive.gearRatio, + config.physicalCharacteristics.conversionFactor.angle.gearRatio, + Volts.of(config.physicalCharacteristics.driveFrictionVoltage), + Volts.of(config.physicalCharacteristics.angleFrictionVoltage), + Inches.of(config.physicalCharacteristics.conversionFactor.drive.diameter / 2), + KilogramSquareMeters.of(0.02), + config.physicalCharacteristics.wheelGripCoefficientOfFriction)); + + mapleSimDrive = new SwerveDriveSimulation(simulationConfig, startingPose); + + // feed module simulation instances to modules + for (int i = 0; i < swerveModules.length; i++) { + this.swerveModules[i].configureModuleSimulation( + mapleSimDrive.getModules()[i], config.physicalCharacteristics); + } + + // register the drivetrain simulation + SimulatedArena.getInstance().addDriveTrainSimulation(mapleSimDrive); + simIMU = new SwerveIMUSimulation(mapleSimDrive.getGyroSimulation()); + imuReadingCache = new Cache<>(simIMU::getGyroRotation3d, 5L); + } else { + imu = config.imu; + imu.factoryDefault(); + imuReadingCache = new Cache<>(imu::getRotation3d, 5L); + } + + // odometry = new SwerveDriveOdometry(kinematics, getYaw(), getModulePositions()); + swerveDrivePoseEstimator = + new SwerveDrivePoseEstimator( + kinematics, + getYaw(), + getModulePositions(), + startingPose); // x,y,heading in radians; Vision measurement std dev, higher=less weight + // + // Rotation3d currentGyro = imuReadingCache.getValue(); + // double offset = currentGyro.getZ() + + // startingPose.getRotation().getRadians(); + // setGyroOffset(new Rotation3d(currentGyro.getX(), currentGyro.getY(), offset)); + + // Initialize Telemetry + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.POSE.ordinal()) { + SmartDashboard.putData("Field", field); + } + + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { + SwerveDriveTelemetry.maxSpeed = maxSpeedMPS; + SwerveDriveTelemetry.maxAngularVelocity = swerveController.config.maxAngularVelocity; + SwerveDriveTelemetry.moduleCount = swerveModules.length; + SwerveDriveTelemetry.sizeFrontBack = + Units.metersToInches( + SwerveMath.getSwerveModule(swerveModules, true, false).moduleLocation.getX() + + SwerveMath.getSwerveModule(swerveModules, false, false).moduleLocation.getX()); + SwerveDriveTelemetry.sizeLeftRight = + Units.metersToInches( + SwerveMath.getSwerveModule(swerveModules, false, true).moduleLocation.getY() + + SwerveMath.getSwerveModule(swerveModules, false, false).moduleLocation.getY()); + SwerveDriveTelemetry.wheelLocations = new double[SwerveDriveTelemetry.moduleCount * 2]; + for (SwerveModule module : swerveModules) { + SwerveDriveTelemetry.wheelLocations[module.moduleNumber * 2] = + Units.metersToInches(module.configuration.moduleLocation.getX()); + SwerveDriveTelemetry.wheelLocations[(module.moduleNumber * 2) + 1] = + Units.metersToInches(module.configuration.moduleLocation.getY()); + } + SwerveDriveTelemetry.measuredStates = new double[SwerveDriveTelemetry.moduleCount * 2]; + SwerveDriveTelemetry.desiredStates = new double[SwerveDriveTelemetry.moduleCount * 2]; + SwerveDriveTelemetry.desiredStatesObj = + new SwerveModuleState[SwerveDriveTelemetry.moduleCount]; + SwerveDriveTelemetry.measuredStatesObj = + new SwerveModuleState[SwerveDriveTelemetry.moduleCount]; + } + + setOdometryPeriod(SwerveDriveTelemetry.isSimulation ? 0.004 : 0.02); + + checkIfTunerXCompatible(); + + HAL.report(kResourceType_RobotDrive, kRobotDriveSwerve_YAGSL); + } + + @Override + public void close() { + imu.close(); + tunerXRecommendation.close(); + + for (var module : swerveModules) { + module.close(); + } + } + + /** + * Update the cache validity period for the robot. + * + * @param imu IMU reading cache validity period in milliseconds. + * @param driveMotor Drive motor reading cache in milliseconds. + * @param absoluteEncoder Absolute encoder reading cache in milliseconds. + */ + public void updateCacheValidityPeriods(long imu, long driveMotor, long absoluteEncoder) { + imuReadingCache.updateValidityPeriod(imu); + for (SwerveModule module : swerveModules) { + module.drivePositionCache.updateValidityPeriod(driveMotor); + module.driveVelocityCache.updateValidityPeriod(driveMotor); + module.absolutePositionCache.updateValidityPeriod(absoluteEncoder); + } + } + + /** Check all components to ensure that Tuner X Swerve Generator is recommended instead. */ + private void checkIfTunerXCompatible() { + boolean compatible = imu instanceof Pigeon2Swerve; + for (SwerveModule module : swerveModules) { + compatible = + compatible + && module.getDriveMotor() instanceof TalonFXSwerve + && module.getAngleMotor() instanceof TalonFXSwerve + && module.getAbsoluteEncoder() instanceof CANCoderSwerve; + if (!compatible) { + break; + } + } + if (compatible) { + tunerXRecommendation.set(true); + } + } + + /** + * Set the odometry update period in seconds. + * + * @param period period in seconds. + */ + public void setOdometryPeriod(double period) { + odometryThread.stop(); + if (SwerveDriveTelemetry.isSimulation) { + SimulatedArena.overrideSimulationTimings(Seconds.of(period), 1); + } + odometryThread.startPeriodic(period); + } + + /** Stop the odometry thread in favor of manually updating odometry. */ + public void stopOdometryThread() { + odometryThread.stop(); + if (SwerveDriveTelemetry.isSimulation) { + SimulatedArena.overrideSimulationTimings(Seconds.of(TimedRobot.kDefaultPeriod), 5); + } + } + + /** + * Set the conversion factor for the angle/azimuth motor controller. + * + * @param conversionFactor Angle motor conversion factor for PID, should be generated from {@link + * SwerveMath#calculateDegreesPerSteeringRotation(double, double)} or calculated. + */ + public void setAngleMotorConversionFactor(double conversionFactor) { + for (SwerveModule module : swerveModules) { + module.setAngleMotorConversionFactor(conversionFactor); + } + } + + /** + * Set the conversion factor for the drive motor controller. + * + * @param conversionFactor Drive motor conversion factor for PID, should be generated from {@link + * SwerveMath#calculateMetersPerRotation(double, double, double)} or calculated. + */ + public void setDriveMotorConversionFactor(double conversionFactor) { + for (SwerveModule module : swerveModules) { + module.setDriveMotorConversionFactor(conversionFactor); + } + } + + /** + * Fetch the latest odometry heading, should be trusted over {@link SwerveDrive#getYaw()}. + * + * @return {@link Rotation2d} of the robot heading. + */ + public Rotation2d getOdometryHeading() { + return swerveDrivePoseEstimator.getEstimatedPosition().getRotation(); + } + + /** + * Set the heading correction capabilities of YAGSL. + * + * @param state {@link SwerveDrive#headingCorrection} state. + */ + public void setHeadingCorrection(boolean state) { + setHeadingCorrection(state, HEADING_CORRECTION_DEADBAND); + } + + /** + * Set the heading correction capabilities of YAGSL. + * + * @param state {@link SwerveDrive#headingCorrection} state. + * @param deadband {@link SwerveDrive#HEADING_CORRECTION_DEADBAND} deadband. + */ + public void setHeadingCorrection(boolean state, double deadband) { + headingCorrection = state; + HEADING_CORRECTION_DEADBAND = deadband; + } + + /** + * Tertiary method of controlling the drive base given velocity in both field oriented and robot + * oriented at the same time. The inputs are added together so this is not intended to be used to + * give the driver both methods of control. + * + * @param fieldOrientedVelocity The field oriented velocties to use + * @param robotOrientedVelocity The robot oriented velocties to use + */ + public void driveFieldOrientedAndRobotOriented( + ChassisSpeeds fieldOrientedVelocity, ChassisSpeeds robotOrientedVelocity) { + + drive( + ChassisSpeeds.fromFieldRelativeSpeeds(fieldOrientedVelocity, getOdometryHeading()) + .plus(robotOrientedVelocity)); + } + + /** + * Secondary method of controlling the drive base given velocity and adjusting it for field + * oriented use. + * + * @param fieldRelativeSpeeds Velocity of the robot desired. + */ + public void driveFieldOriented(ChassisSpeeds fieldRelativeSpeeds) { + drive(ChassisSpeeds.fromFieldRelativeSpeeds(fieldRelativeSpeeds, getOdometryHeading())); + } + + /** + * Secondary method of controlling the drive base given velocity and adjusting it for field + * oriented use. + * + * @param fieldRelativeSpeeds Velocity of the robot desired. + * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. + */ + public void driveFieldOriented( + ChassisSpeeds fieldRelativeSpeeds, Translation2d centerOfRotationMeters) { + drive( + ChassisSpeeds.fromFieldRelativeSpeeds(fieldRelativeSpeeds, getOdometryHeading()), + centerOfRotationMeters); + } + + /** + * Secondary method for controlling the drivebase. Given a simple {@link ChassisSpeeds} set the + * swerve module states, to achieve the goal. + * + * @param velocity The desired robot-oriented {@link ChassisSpeeds} for the robot to achieve. + */ + public void drive(ChassisSpeeds velocity) { + drive(velocity, false, new Translation2d()); + } + + /** + * Secondary method for controlling the drivebase. Given a simple {@link ChassisSpeeds} set the + * swerve module states, to achieve the goal. + * + * @param velocity The desired robot-oriented {@link ChassisSpeeds} for the robot to achieve. + * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. + */ + public void drive(ChassisSpeeds velocity, Translation2d centerOfRotationMeters) { + drive(velocity, false, centerOfRotationMeters); + } + + /** + * The primary method for controlling the drivebase. Takes a {@link Translation2d} and a rotation + * rate, and calculates and commands module states accordingly. Can use either open-loop or + * closed-loop velocity control for the wheel velocities. Also has field- and robot-relative + * modes, which affect how the translation vector is used. + * + * @param translation {@link Translation2d} that is the commanded linear velocity of the robot, in + * meters per second. In robot-relative mode, positive x is torwards the bow (front) and + * positive y is torwards port (left). In field-relative mode, positive x is away from the + * alliance wall (field North) and positive y is torwards the left wall when looking through + * the driver station glass (field West). + * @param rotation Robot angular rate, in radians per second. CCW positive. Unaffected by + * field/robot relativity. + * @param fieldRelative Drive mode. True for field-relative, false for robot-relative. + * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable + * closed-loop. + * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. + */ + public void drive( + Translation2d translation, + double rotation, + boolean fieldRelative, + boolean isOpenLoop, + Translation2d centerOfRotationMeters) { + // Creates a robot-relative ChassisSpeeds object, converting from field-relative speeds if + // necessary. + ChassisSpeeds velocity = new ChassisSpeeds(translation.getX(), translation.getY(), rotation); + if (fieldRelative) { + velocity = ChassisSpeeds.fromFieldRelativeSpeeds(velocity, getOdometryHeading()); + } + drive(velocity, isOpenLoop, centerOfRotationMeters); + } + + /** + * The primary method for controlling the drivebase. Takes a {@link Translation2d} and a rotation + * rate, and calculates and commands module states accordingly. Can use either open-loop or + * closed-loop velocity control for the wheel velocities. Also has field- and robot-relative + * modes, which affect how the translation vector is used. + * + * @param translation {@link Translation2d} that is the commanded linear velocity of the robot, in + * meters per second. In robot-relative mode, positive x is torwards the bow (front) and + * positive y is torwards port (left). In field-relative mode, positive x is away from the + * alliance wall (field North) and positive y is torwards the left wall when looking through + * the driver station glass (field West). + * @param rotation Robot angular rate, in radians per second. CCW positive. Unaffected by + * field/robot relativity. + * @param fieldRelative Drive mode. True for field-relative, false for robot-relative. + * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable + * closed-loop. + */ + public void drive( + Translation2d translation, double rotation, boolean fieldRelative, boolean isOpenLoop) { + // Creates a robot-relative ChassisSpeeds object, converting from field-relative speeds if + // necessary. + ChassisSpeeds velocity = new ChassisSpeeds(translation.getX(), translation.getY(), rotation); + + if (fieldRelative) { + velocity = ChassisSpeeds.fromFieldRelativeSpeeds(velocity, getOdometryHeading()); + } + drive(velocity, isOpenLoop, new Translation2d()); + } + + /** + * The primary method for controlling the drivebase. Takes a {@link ChassisSpeeds}, and calculates + * and commands module states accordingly. Can use either open-loop or closed-loop velocity + * control for the wheel velocities. Applies heading correction if enabled and necessary. + * + * @param robotRelativeVelocity The chassis speeds to set the robot to achieve. + * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable + * closed-loop. + * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. + */ + public void drive( + ChassisSpeeds robotRelativeVelocity, + boolean isOpenLoop, + Translation2d centerOfRotationMeters) { + SwerveDriveTelemetry.startCtrlCycle(); + robotRelativeVelocity = + movementOptimizations( + robotRelativeVelocity, chassisVelocityCorrection, angularVelocityCorrection); + + // Heading Angular Velocity Deadband, might make a configuration option later. + // Originally made by Team 1466 Webb Robotics. + // Modified by Team 7525 Pioneers and BoiledBurntBagel of 6036 + if (headingCorrection) { + if (Math.abs(robotRelativeVelocity.omegaRadiansPerSecond) < HEADING_CORRECTION_DEADBAND + && (Math.abs(robotRelativeVelocity.vxMetersPerSecond) > HEADING_CORRECTION_DEADBAND + || Math.abs(robotRelativeVelocity.vyMetersPerSecond) > HEADING_CORRECTION_DEADBAND)) { + robotRelativeVelocity.omegaRadiansPerSecond = + swerveController.headingCalculate( + getOdometryHeading().getRadians(), lastHeadingRadians); + } else { + lastHeadingRadians = getOdometryHeading().getRadians(); + } + } + + // Display commanded speed for testing + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.LOW.ordinal()) { + SwerveDriveTelemetry.desiredChassisSpeedsObj = robotRelativeVelocity; + } + + // Calculate required module states via kinematics + SwerveModuleState[] swerveModuleStates = + kinematics.toSwerveModuleStates(robotRelativeVelocity, centerOfRotationMeters); + + setRawModuleStates(swerveModuleStates, robotRelativeVelocity, isOpenLoop); + } + + /** + * Set the maximum attainable speeds for desaturation. + * + * @param attainableMaxTranslationalSpeedMetersPerSecond The absolute max speed that your robot + * can reach while translating in meters per second. + * @param attainableMaxRotationalVelocityRadiansPerSecond The absolute max speed the robot can + * reach while rotating in radians per second. + */ + public void setMaximumAttainableSpeeds( + double attainableMaxTranslationalSpeedMetersPerSecond, + double attainableMaxRotationalVelocityRadiansPerSecond) { + this.attainableMaxTranslationalSpeedMetersPerSecond = + attainableMaxTranslationalSpeedMetersPerSecond; + this.attainableMaxRotationalVelocityRadiansPerSecond = + attainableMaxRotationalVelocityRadiansPerSecond; + } + + /** + * Set the maximum allowable speeds for desaturation. + * + * @param maxTranslationalSpeedMetersPerSecond The allowable max speed that your robot should + * reach while translating in meters per second. + * @param maxRotationalVelocityRadiansPerSecond The allowable max speed the robot should reach + * while rotating in radians per second. + */ + public void setMaximumAllowableSpeeds( + double maxTranslationalSpeedMetersPerSecond, double maxRotationalVelocityRadiansPerSecond) { + this.maxChassisSpeedMPS = maxTranslationalSpeedMetersPerSecond; + this.swerveController.config.maxAngularVelocity = maxRotationalVelocityRadiansPerSecond; + } + + /** + * Get the maximum velocity from {@link + * SwerveDrive#attainableMaxTranslationalSpeedMetersPerSecond} or {@link + * SwerveDrive#maxChassisSpeedMPS} whichever is the lower limit on the robot's speed. + * + * @return Minimum speed in meters/second of physically attainable and user allowable limits. + */ + public double getMaximumChassisVelocity() { + return Math.min(this.attainableMaxTranslationalSpeedMetersPerSecond, maxChassisSpeedMPS); + } + + /** + * Get the maximum drive velocity of a module as a {@link LinearVelocity}. + * + * @return {@link LinearVelocity} representing the maximum drive speed of a module. + */ + public double getMaximumModuleDriveVelocity() { + return swerveModules[0].getMaxDriveVelocityMetersPerSecond(); + } + + /** + * Get the maximum angular velocity of an azimuth/angle motor in the swerve module. + * + * @return {@link AngularVelocity} of the maximum azimuth/angle motor. + */ + public AngularVelocity getMaximumModuleAngleVelocity() { + return swerveModules[0].getMaxAngularVelocity(); + } + + /** + * Get the maximum angular velocity, either {@link + * SwerveDrive#attainableMaxRotationalVelocityRadiansPerSecond} or {@link + * SwerveControllerConfiguration#maxAngularVelocity}, whichever is the lower limit on the robot's + * speed. + * + * @return Minimum angular velocity in radians per second of physically attainable and user + * allowable limits. + */ + public double getMaximumChassisAngularVelocity() { + return Math.min( + this.attainableMaxRotationalVelocityRadiansPerSecond, + swerveController.config.maxAngularVelocity); + } + + /** + * Set the module states (azimuth and velocity) directly. + * + * @param desiredStates A list of SwerveModuleStates to send to the modules. + * @param desiredChassisSpeed The desired chassis speeds to set the robot to achieve. + * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable + * closed-loop. + */ + private void setRawModuleStates( + SwerveModuleState[] desiredStates, ChassisSpeeds desiredChassisSpeed, boolean isOpenLoop) { + // Desaturates wheel speeds + double maxModuleSpeedMPS = getMaximumModuleDriveVelocity(); + if ((attainableMaxTranslationalSpeedMetersPerSecond != 0 + || attainableMaxRotationalVelocityRadiansPerSecond != 0) + && attainableMaxTranslationalSpeedMetersPerSecond != maxChassisSpeedMPS) { + SwerveDriveKinematics.desaturateWheelSpeeds( + desiredStates, + desiredChassisSpeed, + maxModuleSpeedMPS, + attainableMaxTranslationalSpeedMetersPerSecond, + attainableMaxRotationalVelocityRadiansPerSecond); + } else { + SwerveDriveKinematics.desaturateWheelSpeeds(desiredStates, maxModuleSpeedMPS); + } + + // Sets states + for (SwerveModule module : swerveModules) { + module.setDesiredState(desiredStates[module.moduleNumber], isOpenLoop, false); + } + } + + /** + * Set the module states (azimuth and velocity) directly. Used primarily for auto paths. Does not + * allow for usage of {@link SwerveDriveKinematics#desaturateWheelSpeeds(SwerveModuleState[] + * moduleStates, ChassisSpeeds desiredChassisSpeed, double + * attainableMaxModuleSpeedMetersPerSecond, double attainableMaxTranslationalSpeedMetersPerSecond, + * double attainableMaxRotationalVelocityRadiansPerSecond)} + * + * @param desiredStates A list of SwerveModuleStates to send to the modules. + * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable + * closed-loop. + */ + public void setModuleStates(SwerveModuleState[] desiredStates, boolean isOpenLoop) { + SwerveDriveTelemetry.startCtrlCycle(); + double maxModuleSpeedMPS = getMaximumModuleDriveVelocity(); + desiredStates = kinematics.toSwerveModuleStates(kinematics.toChassisSpeeds(desiredStates)); + SwerveDriveKinematics.desaturateWheelSpeeds(desiredStates, maxModuleSpeedMPS); + + // Sets states + for (SwerveModule module : swerveModules) { + module.setDesiredState(desiredStates[module.moduleNumber], isOpenLoop, false); + } + } + + /** + * Drive the robot using the {@link SwerveModuleState}, it is recommended to have {@link + * SwerveDrive#setCosineCompensator(boolean)} set to false for this.
+ * + * @param robotRelativeVelocity Robot relative {@link ChassisSpeeds} + * @param states Corresponding {@link SwerveModuleState} to use (not checked against the {@param + * robotRelativeVelocity}). + * @param feedforwardForces Feedforward forces generated by set-point generator + */ + public void drive( + ChassisSpeeds robotRelativeVelocity, SwerveModuleState[] states, Force[] feedforwardForces) { + SwerveDriveTelemetry.startCtrlCycle(); + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.LOW.ordinal()) { + SwerveDriveTelemetry.desiredChassisSpeedsObj = robotRelativeVelocity; + } + for (SwerveModule module : swerveModules) { + module.applyStateOptimizations(states[module.moduleNumber]); + module.applyAntiJitter(states[module.moduleNumber], false); + + // from the module configuration, obtain necessary information to calculate feed-forward + // Warning: Will not work well if motor is not what we are expecting. + // Warning: Should replace module.getDriveMotor().simMotor with expected motor type first. + DCMotor driveMotorModel = module.configuration.driveMotor.getSimMotor(); + double driveGearRatio = module.configuration.conversionFactors.drive.gearRatio; + double wheelRadiusMeters = + Units.inchesToMeters(module.configuration.conversionFactors.drive.diameter) / 2; + + // calculation: + double desiredGroundSpeedMPS = states[module.moduleNumber].speedMetersPerSecond; + double feedforwardVoltage = + driveMotorModel.getVoltage( + // Since: (1) torque = force * momentOfForce; (2) torque (on wheel) = torque (on + // motor) * gearRatio + // torque (on motor) = force * wheelRadius / gearRatio + feedforwardForces[module.moduleNumber].in(Newtons) + * wheelRadiusMeters + / driveGearRatio, + // Since: (1) linear velocity = angularVelocity * wheelRadius; (2) wheelVelocity = + // motorVelocity / gearRatio + // motorAngularVelocity = linearVelocity / wheelRadius * gearRatio + desiredGroundSpeedMPS / wheelRadiusMeters * driveGearRatio); + module.setDesiredState(states[module.moduleNumber], false, feedforwardVoltage); + } + } + + /** + * Set chassis speeds with closed-loop velocity control. + * + * @param robotRelativeSpeeds Chassis speeds to set. + */ + public void setChassisSpeeds(ChassisSpeeds robotRelativeSpeeds) { + SwerveDriveTelemetry.startCtrlCycle(); + robotRelativeSpeeds = + movementOptimizations( + robotRelativeSpeeds, + autonomousChassisVelocityCorrection, + autonomousAngularVelocityCorrection); + + SwerveDriveTelemetry.desiredChassisSpeedsObj = robotRelativeSpeeds; + + setRawModuleStates( + kinematics.toSwerveModuleStates(robotRelativeSpeeds), robotRelativeSpeeds, false); + } + + /** + * Gets the measured pose (position and rotation) of the robot, as reported by odometry. + * + * @return The robot's pose + */ + public Pose2d getPose() { + + odometryLock.lock(); + Pose2d poseEstimation = swerveDrivePoseEstimator.getEstimatedPosition(); + odometryLock.unlock(); + return poseEstimation; + } + + /** + * Gets the maple-sim drivetrain simulation instance This is used to add intake simulation / + * launch game pieces from the robot + * + * @return an optional maple-sim {@link SwerveDriveSimulation} object, or {@link Optional#empty()} + * when calling from a real robot + */ + public Optional getMapleSimDrive() { + if (SwerveDriveTelemetry.isSimulation) { + return Optional.of(mapleSimDrive); + } + + return Optional.empty(); + } + + /** + * Gets the actual pose of the drivetrain during simulation + * + * @return an {@link Optional} {@link Pose2d}, representing the drivetrain pose during simulation, + * or an empty optional when running on real robot + */ + public Optional getSimulationDriveTrainPose() { + odometryLock.lock(); + Optional simulationPose = + getMapleSimDrive().map(AbstractDriveTrainSimulation::getSimulatedDriveTrainPose); + odometryLock.unlock(); + return simulationPose; + } + + /** + * Gets the measured field-relative robot velocity (x, y and omega) + * + * @return A ChassisSpeeds object of the current field-relative velocity + */ + public ChassisSpeeds getFieldVelocity() { + // ChassisSpeeds has a method to convert from field-relative to robot-relative speeds, + // but not the reverse. However, because this transform is a simple rotation, negating the + // angle given as the robot angle reverses the direction of rotation, and the conversion is + // reversed. + ChassisSpeeds robotRelativeSpeeds = kinematics.toChassisSpeeds(getStates()); + return ChassisSpeeds.fromRobotRelativeSpeeds(robotRelativeSpeeds, getOdometryHeading()); + // Might need to be this instead + // return ChassisSpeeds.fromFieldRelativeSpeeds( + // kinematics.toChassisSpeeds(getStates()), getOdometryHeading().unaryMinus()); + } + + /** + * Gets the current robot-relative velocity (x, y and omega) of the robot + * + * @return A ChassisSpeeds object of the current robot-relative velocity + */ + public ChassisSpeeds getRobotVelocity() { + return kinematics.toChassisSpeeds(getStates()); + } + + /** + * Resets odometry to the given pose. Gyro angle and module positions do not need to be reset when + * calling this method. However, if either gyro angle or module position is reset, this must be + * called in order for odometry to keep working. + * + * @param pose The pose to set the odometry to. Field relative, blue-origin where 0deg is facing + * towards RED alliance. + */ + public void resetOdometry(Pose2d pose) { + odometryLock.lock(); + swerveDrivePoseEstimator.resetPosition(getYaw(), getModulePositions(), pose); + if (SwerveDriveTelemetry.isSimulation) { + mapleSimDrive.setSimulationWorldPose(pose); + } + odometryLock.unlock(); + ChassisSpeeds robotRelativeSpeeds = + ChassisSpeeds.fromRobotRelativeSpeeds(new ChassisSpeeds(0, 0, 0), getYaw()); + kinematics.toSwerveModuleStates(robotRelativeSpeeds); + } + + /** + * Post the trajectory to the field + * + * @param trajectory the trajectory to post. + */ + public void postTrajectory(Trajectory trajectory) { + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.POSE.ordinal()) { + field.getObject("Trajectory").setTrajectory(trajectory); + } + } + + /** + * Gets the current module states (azimuth and velocity) + * + * @return A list of SwerveModuleStates containing the current module states + */ + public SwerveModuleState[] getStates() { + SwerveModuleState[] states = new SwerveModuleState[swerveDriveConfiguration.moduleCount]; + for (SwerveModule module : swerveModules) { + states[module.moduleNumber] = module.getState(); + } + return states; + } + + /** + * Gets the current module positions (azimuth and wheel position (meters)). + * + * @return A list of SwerveModulePositions containg the current module positions + */ + public SwerveModulePosition[] getModulePositions() { + SwerveModulePosition[] positions = + new SwerveModulePosition[swerveDriveConfiguration.moduleCount]; + for (SwerveModule module : swerveModules) { + positions[module.moduleNumber] = module.getPosition(); + } + return positions; + } + + /** + * Getter for the {@link SwerveIMU}. + * + * @return generated {@link SwerveIMU} + */ + public SwerveIMU getGyro() { + return swerveDriveConfiguration.imu; + } + + /** + * Set the expected gyroscope angle using a {@link Rotation3d} object. To reset gyro, set to a new + * {@link Rotation3d} subtracted from the current gyroscopic readings {@link + * SwerveIMU#getRotation3d()}. + * + * @param gyro expected gyroscope angle as {@link Rotation3d}. + */ + public void setGyro(Rotation3d gyro) { + if (SwerveDriveTelemetry.isSimulation) { + setGyroOffset(simIMU.getGyroRotation3d().minus(gyro)); + } else { + setGyroOffset(imu.getRawRotation3d().minus(gyro)); + } + imuReadingCache.update(); + } + + /** + * Resets the gyro angle to zero and resets odometry to the same position, but facing toward 0 + * (red alliance station). + */ + public void zeroGyro() { + // Resets the real gyro or the angle accumulator, depending on whether the robot is being + // simulated + if (SwerveDriveTelemetry.isSimulation) { + simIMU.setAngle(0); + } else { + setGyroOffset(imu.getRawRotation3d()); + } + imuReadingCache.update(); + swerveController.lastAngleScalar = 0; + lastHeadingRadians = 0; + resetOdometry(new Pose2d(getPose().getTranslation(), new Rotation2d())); + } + + /** + * Gets the current yaw angle of the robot, as reported by the imu. CCW positive, not wrapped. + * + * @return The yaw as a {@link Rotation2d} angle + */ + public Rotation2d getYaw() { + // Read the imu if the robot is real or the accumulator if the robot is simulated. + return Rotation2d.fromRadians(imuReadingCache.getValue().getZ()); + } + + /** + * Gets the current pitch angle of the robot, as reported by the imu. + * + * @return The heading as a {@link Rotation2d} angle + */ + public Rotation2d getPitch() { + // Read the imu if the robot is real or the accumulator if the robot is simulated. + return Rotation2d.fromRadians(imuReadingCache.getValue().getY()); + } + + /** + * Gets the current roll angle of the robot, as reported by the imu. + * + * @return The heading as a {@link Rotation2d} angle + */ + public Rotation2d getRoll() { + // Read the imu if the robot is real or the accumulator if the robot is simulated. + return Rotation2d.fromRadians(imuReadingCache.getValue().getX()); + } + + /** + * Gets the current gyro {@link Rotation3d} of the robot, as reported by the imu. + * + * @return The heading as a {@link Rotation3d} angle + */ + public Rotation3d getGyroRotation3d() { + // Read the imu if the robot is real or the accumulator if the robot is simulated. + return imuReadingCache.getValue(); + } + + /** + * Gets current acceleration of the robot in m/s/s. If gyro unsupported returns empty. + * + * @return acceleration of the robot as a {@link Translation3d} + */ + public Optional getAccel() { + if (!SwerveDriveTelemetry.isSimulation) { + return imu.getAccel(); + } else { + return simIMU.getAccel(); + } + } + + /** + * Sets the drive motors to brake/coast mode. + * + * @param brake True to set motors to brake mode, false for coast. + */ + public void setMotorIdleMode(boolean brake) { + for (SwerveModule swerveModule : swerveModules) { + swerveModule.setMotorBrake(brake); + } + } + + /** + * Enable auto synchronization for encoders during a match. This will only occur when the modules + * are not moving for a few seconds. + * + * @param enabled Enable state + * @param deadband Deadband in degrees, default is 3 degrees. + */ + public void setModuleEncoderAutoSynchronize(boolean enabled, double deadband) { + for (SwerveModule swerveModule : swerveModules) { + swerveModule.setEncoderAutoSynchronize(enabled, deadband); + } + } + + /** + * Point all modules toward the robot center, thus making the robot very difficult to move. + * Forcing the robot to keep the current pose. + */ + public void lockPose() { + // Sets states + for (SwerveModule swerveModule : swerveModules) { + SwerveModuleState desiredState = + new SwerveModuleState(0, swerveModule.configuration.moduleLocation.getAngle()); + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { + SwerveDriveTelemetry.desiredStatesObj[swerveModule.moduleNumber] = desiredState; + } + swerveModule.setDesiredState(desiredState, false, true); + } + + // Update kinematics because we are not using setModuleStates + kinematics.toSwerveModuleStates(new ChassisSpeeds()); + } + + /** + * Get the swerve module poses and on the field relative to the robot. + * + * @param robotPose Robot pose. + * @return Swerve module poses. + */ + public Pose2d[] getSwerveModulePoses(Pose2d robotPose) { + Pose2d[] poseArr = new Pose2d[swerveDriveConfiguration.moduleCount]; + List poses = new ArrayList<>(); + for (SwerveModule module : swerveModules) { + poses.add( + robotPose.plus( + new Transform2d(module.configuration.moduleLocation, module.getState().angle))); + } + return poses.toArray(poseArr); + } + + /** + * Setup the swerve module feedforward. + * + * @param driveFeedforward Feedforward for the drive motor on swerve modules. + */ + public void replaceSwerveModuleFeedforward(SimpleMotorFeedforward driveFeedforward) { + for (SwerveModule swerveModule : swerveModules) { + swerveModule.setFeedforward(driveFeedforward); + } + } + + /** + * Update odometry should be run every loop. Synchronizes module absolute encoders with relative + * encoders periodically. In simulation mode will also post the pose of each module. Updates + * SmartDashboard with module encoder readings and states. + */ + public void updateOdometry() { + SwerveDriveTelemetry.startOdomCycle(); + odometryLock.lock(); + // invalidateCache(); + try { + // Update odometry + swerveDrivePoseEstimator.update(getYaw(), getModulePositions()); + + if (SwerveDriveTelemetry.isSimulation) { + try { + SimulatedArena.getInstance().simulationPeriodic(); + } catch (Exception e) { + DriverStation.reportError("MapleSim error", false); + } + } + + // Update angle accumulator if the robot is simulated + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { + SwerveDriveTelemetry.measuredChassisSpeedsObj = getRobotVelocity(); + SwerveDriveTelemetry.robotRotationObj = getOdometryHeading(); + } + + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.POSE.ordinal()) { + if (SwerveDriveTelemetry.isSimulation) { + field.setRobotPose(mapleSimDrive.getSimulatedDriveTrainPose()); + field.getObject("OdometryPose").setPose(swerveDrivePoseEstimator.getEstimatedPosition()); + field + .getObject("XModules") + .setPoses(getSwerveModulePoses(mapleSimDrive.getSimulatedDriveTrainPose())); + + } else { + field.setRobotPose(swerveDrivePoseEstimator.getEstimatedPosition()); + } + } + + double sumVelocity = 0; + for (SwerveModule module : swerveModules) { + SwerveModuleState moduleState = module.getState(); + sumVelocity += Math.abs(moduleState.speedMetersPerSecond); + if (SwerveDriveTelemetry.verbosity == TelemetryVerbosity.HIGH) { + module.updateTelemetry(); + rawIMUPublisher.set(getYaw().getDegrees()); + adjustedIMUPublisher.set(getOdometryHeading().getDegrees()); + } + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { + SwerveDriveTelemetry.measuredStatesObj[module.moduleNumber] = moduleState; + } + } + + // If the robot isn't moving synchronize the encoders every 100ms (Inspired by democrat's SDS + // lib) + // To ensure that everytime we initialize it works. + if (sumVelocity <= .01 && ++moduleSynchronizationCounter > 5) { + synchronizeModuleEncoders(); + moduleSynchronizationCounter = 0; + } + + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { + SwerveDriveTelemetry.updateData(); + } + } catch (Exception e) { + odometryLock.unlock(); + throw e; + } + odometryLock.unlock(); + SwerveDriveTelemetry.endOdomCycle(); + } + + /** Invalidate all {@link Cache} object used by the {@link SwerveDrive} */ + public void invalidateCache() { + imuReadingCache.update(); + for (SwerveModule module : swerveModules) { + module.invalidateCache(); + } + } + + /** Synchronize angle motor integrated encoders with data from absolute encoders. */ + public void synchronizeModuleEncoders() { + for (SwerveModule module : swerveModules) { + module.queueSynchronizeEncoders(); + } + } + + /** + * Set the gyro scope offset to a desired known rotation. Unlike {@link + * SwerveDrive#setGyro(Rotation3d)} it DOES NOT take the current rotation into account. + * + * @param offset {@link Rotation3d} known offset of the robot for gyroscope to use. + */ + public void setGyroOffset(Rotation3d offset) { + if (SwerveDriveTelemetry.isSimulation) { + simIMU.setAngle(offset.getZ()); + } else { + imu.setOffset(offset); + } + imuReadingCache.update(); + } + + /** + * Add a vision measurement to the {@link SwerveDrivePoseEstimator} and update the {@link + * SwerveIMU} gyro reading with the given timestamp of the vision measurement. + * + * @param robotPose Robot {@link Pose2d} as measured by vision. + * @param timestamp Timestamp the measurement was taken as time since startup, should be taken + * from {@link Timer#getFPGATimestamp()} or similar sources. + * @param visionMeasurementStdDevs Vision measurement standard deviation that will be sent to the + * {@link SwerveDrivePoseEstimator}.The standard deviation of the vision measurement, for best + * accuracy calculate the standard deviation at 2 or more points and fit a line to it with the + * calculated optimal standard deviation. (Units should be meters per pixel). By optimizing + * this you can get * vision accurate to inches instead of feet. + */ + public void addVisionMeasurement( + Pose2d robotPose, double timestamp, Matrix visionMeasurementStdDevs) { + odometryLock.lock(); + swerveDrivePoseEstimator.addVisionMeasurement(robotPose, timestamp, visionMeasurementStdDevs); + odometryLock.unlock(); + } + + /** + * Sets the pose estimator's trust of global measurements. This might be used to change trust in + * vision measurements after the autonomous period, or to change trust as distance to a vision + * target increases. + * + * @param visionMeasurementStdDevs Standard deviations of the vision measurements. Increase these + * numbers to trust global measurements from vision less. This matrix is in the form [x, y, + * theta], with units in meters and radians. + */ + public void setVisionMeasurementStdDevs(Matrix visionMeasurementStdDevs) { + odometryLock.lock(); + swerveDrivePoseEstimator.setVisionMeasurementStdDevs(visionMeasurementStdDevs); + odometryLock.unlock(); + } + + /** + * Add a vision measurement to the {@link SwerveDrivePoseEstimator} and update the {@link + * SwerveIMU} gyro reading with the given timestamp of the vision measurement. + * + * @param robotPose Robot {@link Pose2d} as measured by vision. + * @param timestamp Timestamp the measurement was taken as time since startup, should be taken + * from {@link Timer#getFPGATimestamp()} or similar sources. + */ + public void addVisionMeasurement(Pose2d robotPose, double timestamp) { + odometryLock.lock(); + swerveDrivePoseEstimator.addVisionMeasurement(robotPose, timestamp); + // Pose2d newOdometry = new + // Pose2d(swerveDrivePoseEstimator.getEstimatedPosition().getTranslation(), + // robotPose.getRotation()); + odometryLock.unlock(); + + // setGyroOffset(new Rotation3d(0, 0, robotPose.getRotation().getRadians())); + // resetOdometry(newOdometry); + } + + /** + * Helper function to get the {@link SwerveDrive#swerveController} for the {@link SwerveDrive} + * which can be used to generate {@link ChassisSpeeds} for the robot to orient it correctly given + * axis or angles, and apply {@link edu.wpi.first.math.filter.SlewRateLimiter} to given inputs. + * Important functions to look at are {@link SwerveController#getTargetSpeeds(double, double, + * double, double, double)}, {@link SwerveController#addSlewRateLimiters(SlewRateLimiter, + * SlewRateLimiter, SlewRateLimiter)}, {@link SwerveController#getRawTargetSpeeds(double, double, + * double)}. + * + * @return {@link SwerveController} for the {@link SwerveDrive}. + */ + public SwerveController getSwerveController() { + return swerveController; + } + + /** + * Get the {@link SwerveModule}s associated with the {@link SwerveDrive}. + * + * @return {@link SwerveModule} array specified by configurations. + */ + public SwerveModule[] getModules() { + return swerveDriveConfiguration.modules; + } + + /** + * Get the {@link SwerveModule}'s as a {@link HashMap} where the key is the swerve module + * configuration name. + * + * @return {@link HashMap}(Module Name, SwerveModule) + */ + public Map getModuleMap() { + Map map = new HashMap(); + for (SwerveModule module : swerveModules) { + map.put(module.configuration.name, module); + } + return map; + } + + /** + * Reset the drive encoders on the robot, useful when manually resetting the robot without a + * reboot, like in autonomous. + */ + public void resetDriveEncoders() { + for (SwerveModule module : swerveModules) { + module.getDriveMotor().setPosition(0); + } + } + + /** + * Set the motor controller closed loop feedback device to the defined external absolute encoder, + * with the given offset from the supplied configuration, overwriting any native offset. + */ + public void useExternalFeedbackSensor() { + for (SwerveModule module : swerveModules) { + module.useExternalFeedbackSensor(); + } + } + + /** + * Set the motor controller closed loop feedback device to the internal encoder instead of the + * absolute encoder. + */ + public void useInternalFeedbackSensor() { + for (SwerveModule module : swerveModules) { + module.useInternalFeedbackSensor(); + } + } + + /** + * Pushes the Absolute Encoder offsets to the Encoder or Motor Controller, depending on type. Also + * removes the internal offsets to prevent double offsetting. + */ + @Deprecated + public void pushOffsetsToEncoders() { + for (SwerveModule module : swerveModules) { + module.pushOffsetsToEncoders(); + } + } + + /** Restores Internal YAGSL Encoder offsets and sets the Encoder stored offset back to 0 */ + @Deprecated + public void restoreInternalOffset() { + for (SwerveModule module : swerveModules) { + module.restoreInternalOffset(); + } + } + + /** + * Set module optimization to be utilized or not. Sometimes it is desirable to be enabled for + * debugging purposes only. + * + * @param enabled Optimization enabled state. + */ + public void setModuleStateOptimization(boolean enabled) { + for (SwerveModule module : swerveModules) { + module.setModuleStateOptimization(enabled); + } + } + + /** + * Enable auto-centering module wheels. This has a side effect of causing some jitter to the robot + * when a PID is not tuned perfectly. This function is a wrapper for {@link + * SwerveModule#setAntiJitter(boolean)} to perform auto-centering. + * + * @param enabled Enable auto-centering (disable antiJitter) + */ + public void setAutoCenteringModules(boolean enabled) { + for (SwerveModule module : swerveModules) { + module.setAntiJitter(!enabled); + } + } + + /** + * Enable or disable the {@link swervelib.parser.SwerveModuleConfiguration#useCosineCompensator} + * for all {@link SwerveModule}'s in the swerve drive. The cosine compensator will slow down or + * speed up modules that are close to their desired state in theory. + * + * @param enabled Usage of the cosine compensator. + */ + public void setCosineCompensator(boolean enabled) { + for (SwerveModule module : swerveModules) { + module.configuration.useCosineCompensator = enabled; + } + } + + /** + * Sets the Chassis discretization seconds as well as enableing/disabling the Chassis velocity + * correction in teleop + * + * @param enable Enable chassis velocity correction, which will use {@link + * ChassisSpeeds#discretize(ChassisSpeeds, double)}} with the following. + * @param dtSeconds The duration of the timestep the speeds should be applied for. + */ + public void setChassisDiscretization(boolean enable, double dtSeconds) { + if (!SwerveDriveTelemetry.isSimulation) { + chassisVelocityCorrection = enable; + discretizationdtSeconds = dtSeconds; + } + } + + /** + * Sets the Chassis discretization seconds as well as enableing/disabling the Chassis velocity + * correction in teleop and/or auto + * + * @param useInTeleop Enable chassis velocity correction, which will use {@link + * ChassisSpeeds#discretize(ChassisSpeeds, double)} with the following in teleop. + * @param useInAuto Enable chassis velocity correction, which will use {@link + * ChassisSpeeds#discretize(ChassisSpeeds, double)} with the following in auto. + * @param dtSeconds The duration of the timestep the speeds should be applied for. + */ + public void setChassisDiscretization(boolean useInTeleop, boolean useInAuto, double dtSeconds) { + if (!SwerveDriveTelemetry.isSimulation) { + chassisVelocityCorrection = useInTeleop; + autonomousChassisVelocityCorrection = useInAuto; + discretizationdtSeconds = dtSeconds; + } + } + + /** + * Enables angular velocity skew correction in teleop and/or autonomous and sets the angular + * velocity coefficient for both modes + * + * @param useInTeleop Enables angular velocity correction in teleop. + * @param useInAuto Enables angular velocity correction in autonomous. + * @param angularVelocityCoeff The angular velocity coefficient. Expected values between -0.15 to + * 0.15. Start with a value of 0.1, test in teleop. When enabling for the first time if the + * skew is significantly worse try inverting the value. Tune by moving in a straight line + * while rotating. Testing is best done with angular velocity controls on the right stick. + * Change the value until you are visually happy with the skew. Ensure your tune works with + * different translational and rotational magnitudes. If this reduces skew in teleop, it may + * improve auto. + */ + public void setAngularVelocityCompensation( + boolean useInTeleop, boolean useInAuto, double angularVelocityCoeff) { + if (!SwerveDriveTelemetry.isSimulation) { + angularVelocityCorrection = useInTeleop; + autonomousAngularVelocityCorrection = useInAuto; + angularVelocityCoefficient = angularVelocityCoeff; + } + } + + /** + * Correct for skew that worsens as angular velocity increases + * + * @param robotRelativeVelocity The chassis speeds to set the robot to achieve. + * @return {@link ChassisSpeeds} of the robot after angular velocity skew correction. + */ + public ChassisSpeeds angularVelocitySkewCorrection(ChassisSpeeds robotRelativeVelocity) { + var angularVelocity = + new Rotation2d( + imu.getYawAngularVelocity().in(RadiansPerSecond) * angularVelocityCoefficient); + if (angularVelocity.getRadians() != 0.0) { + ChassisSpeeds fieldRelativeVelocity = + ChassisSpeeds.fromRobotRelativeSpeeds(robotRelativeVelocity, getOdometryHeading()); + robotRelativeVelocity = + ChassisSpeeds.fromFieldRelativeSpeeds( + fieldRelativeVelocity, getOdometryHeading().plus(angularVelocity)); + } + return robotRelativeVelocity; + } + + /** + * Enable desired drive corrections + * + * @param robotRelativeVelocity The chassis speeds to set the robot to achieve. + * @param uesChassisDiscretize Correct chassis velocity using 254's correction. + * @param useAngularVelocitySkewCorrection Use the robot's angular velocity to correct for skew. + * @return The chassis speeds after optimizations. + */ + private ChassisSpeeds movementOptimizations( + ChassisSpeeds robotRelativeVelocity, + boolean uesChassisDiscretize, + boolean useAngularVelocitySkewCorrection) { + + if (useAngularVelocitySkewCorrection) { + robotRelativeVelocity = angularVelocitySkewCorrection(robotRelativeVelocity); + } + + // Thank you to Jared Russell FRC254 for Open Loop Compensation Code + // https://www.chiefdelphi.com/t/whitepaper-swerve-drive-skew-and-second-order-kinematics/416964/5 + if (uesChassisDiscretize) { + robotRelativeVelocity = + ChassisSpeeds.discretize(robotRelativeVelocity, discretizationdtSeconds); + } + + return robotRelativeVelocity; + } + + /** + * Convert a {@link ChassisSpeeds} to {@link SwerveModuleState[]} for use elsewhere. + * + * @param robotRelativeVelocity {@link ChassisSpeeds} velocity to use. + * @param optimize Perform chassis velocity correction or angular velocity correction. + * @return {@link SwerveModuleState[]} for use elsewhere. + */ + public SwerveModuleState[] toServeModuleStates( + ChassisSpeeds robotRelativeVelocity, boolean optimize) { + if (optimize) { + robotRelativeVelocity = + movementOptimizations( + robotRelativeVelocity, chassisVelocityCorrection, angularVelocityCorrection); + } + return kinematics.toSwerveModuleStates(robotRelativeVelocity); + } +} diff --git a/src/main/java/swervelib/SwerveDriveTest.java b/src/main/java/swervelib/SwerveDriveTest.java new file mode 100644 index 00000000..3e979a35 --- /dev/null +++ b/src/main/java/swervelib/SwerveDriveTest.java @@ -0,0 +1,477 @@ +package swervelib; + +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.Meter; +import static edu.wpi.first.units.Units.Meters; +import static edu.wpi.first.units.Units.MetersPerSecond; +import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.units.Units.Volts; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.units.measure.MutAngle; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.units.measure.MutDistance; +import edu.wpi.first.units.measure.MutLinearVelocity; +import edu.wpi.first.units.measure.MutVoltage; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import edu.wpi.first.wpilibj.sysid.SysIdRoutineLog; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Config; +import java.util.function.Supplier; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** Class to perform tests on the swerve drive. */ +public class SwerveDriveTest { + + /** Tracks the voltage being applied to a motor */ + private static final MutVoltage m_appliedVoltage = new MutVoltage(0, 0, Volts); + /** Tracks the distance travelled of a position motor */ + private static final MutDistance m_distance = new MutDistance(0, 0, Meter); + /** Tracks the velocity of a positional motor */ + private static final MutLinearVelocity m_velocity = new MutLinearVelocity(0, 9, MetersPerSecond); + /** Tracks the rotations of an angular motor */ + private static final MutAngle m_anglePosition = new MutAngle(0, 0, Degrees); + /** Tracks the velocity of an angular motor */ + private static final MutAngularVelocity m_angVelocity = + new MutAngularVelocity(0, 0, DegreesPerSecond); + + /** + * Set the angle of the modules to a given {@link Rotation2d} + * + * @param swerveDrive {@link SwerveDrive} to use. + * @param moduleAngle {@link Rotation2d} to set every module to. + */ + public static void angleModules(SwerveDrive swerveDrive, Rotation2d moduleAngle) { + for (SwerveModule swerveModule : swerveDrive.getModules()) { + swerveModule.getAngleMotor().setReference(moduleAngle.getDegrees(), 0); + } + } + + /** + * Power the drive motors for the swerve drive to a set duty cycle percentage. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param percentage Duty cycle percentage of voltage to send to drive motors. + */ + public static void powerDriveMotorsDutyCycle(SwerveDrive swerveDrive, double percentage) { + for (SwerveModule swerveModule : swerveDrive.getModules()) { + swerveModule.getDriveMotor().set(percentage); + } + } + + /** + * Power the angle motors for the swerve drive to a set percentage. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param percentage DutyCycle percentage to send to angle motors. + */ + public static void powerAngleMotorsDutyCycle(SwerveDrive swerveDrive, double percentage) { + for (SwerveModule swerveModule : swerveDrive.getModules()) { + swerveModule.getAngleMotor().set(percentage); + } + } + + /** + * Power the drive motors for the swerve drive to a set voltage. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param volts Voltage to send to drive motors. + */ + public static void powerDriveMotorsVoltage(SwerveDrive swerveDrive, double volts) { + for (SwerveModule swerveModule : swerveDrive.getModules()) { + swerveModule.getDriveMotor().setVoltage(volts); + } + } + + /** + * Power the angle motors for the swerve drive to a set voltage. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param volts Voltage to send to angle motors. + */ + public static void powerAngleMotorsVoltage(SwerveDrive swerveDrive, double volts) { + for (SwerveModule swerveModule : swerveDrive.getModules()) { + swerveModule.getAngleMotor().setVoltage(volts); + } + } + + /** + * Set the modules to center to 0. + * + * @param swerveDrive Swerve Drive to control. + */ + public static void centerModules(SwerveDrive swerveDrive) { + angleModules(swerveDrive, Rotation2d.fromDegrees(0)); + } + + /** + * Set the modules to their rotary position to allow running sysid and spinning the robot + * + * @param swerveDrive Swerve Drive to control. + */ + public static void setModulesToRotaryPosition(SwerveDrive swerveDrive) { + SwerveModuleState[] rotaryStates = + swerveDrive.kinematics.toSwerveModuleStates(new ChassisSpeeds(0, 0, 1)); + for (int i = 0; i < swerveDrive.getModules().length; i++) { + swerveDrive + .getModules()[i] + .getAngleMotor() + .setReference(rotaryStates[i].angle.getDegrees(), 0); + } + } + + /** + * Set the sim modules to center to 0 and power them to drive in a voltage. Calling this function + * in sim is equivalent to calling {@link #centerModules(SwerveDrive)} and {@link + * #powerDriveMotorsVoltage(SwerveDrive, double)} on a real robot. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param volts Voltage to send to drive motors. + * @param testWithSpinning - Whether to make the robot spin in place instead of driving in a + * straight line, true to make the robot spin, false to make the robot drive in straight line + */ + public static void runDriveMotorsCharacterizationOnSimModules( + SwerveDrive swerveDrive, double volts, boolean testWithSpinning) { + SwerveModuleState[] rotaryStates = + swerveDrive.kinematics.toSwerveModuleStates(new ChassisSpeeds(0, 0, 1)); + for (int i = 0; i < swerveDrive.getModules().length; i++) { + swerveDrive + .getModules()[i] + .getSimModule() + .runDriveMotorCharacterization( + testWithSpinning ? rotaryStates[i].angle : Rotation2d.kZero, volts); + } + } + + /** + * Set the sim modules to center to 0 and power them to drive in a voltage. Calling this function + * in sim is equivalent to calling {@link #centerModules(SwerveDrive)} and {@link + * #powerDriveMotorsVoltage(SwerveDrive, double)} on a real robot. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param volts Voltage to send to angle motors. + */ + public static void runAngleMotorsCharacterizationOnSimModules( + SwerveDrive swerveDrive, double volts) { + for (SwerveModule module : swerveDrive.getModules()) { + module.getSimModule().runAngleMotorCharacterization(volts); + } + } + + /** + * Find the minimum amount of power required to move the swerve drive motors. + * + * @param swerveDrive {@link SwerveDrive} to control. + * @param minMovement Minimum amount of movement to drive motors. + * @param testDelaySeconds Time in seconds for the motor to move. + * @param maxVolts The maximum voltage to send to drive motors. + * @return minimum voltage required. + */ + public static double findDriveMotorKV( + SwerveDrive swerveDrive, double minMovement, double testDelaySeconds, double maxVolts) { + double[] startingEncoders = new double[4]; + double kV = 0; + + SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); + SwerveModule[] modules = swerveDrive.getModules(); + for (int i = 0; i < modules.length; i++) { + startingEncoders[i] = Math.abs(modules[i].getDriveMotor().getPosition()); + } + + for (double kV_new = 0; kV_new < maxVolts; kV_new += 0.0001) { + + SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, kV); + boolean foundkV = false; + double startTimeSeconds = Timer.getFPGATimestamp(); + while ((Timer.getFPGATimestamp() - startTimeSeconds) < testDelaySeconds && !foundkV) { + for (int i = 0; i < modules.length; i++) { + if ((modules[i].getDriveMotor().getPosition() - startingEncoders[i]) > minMovement) { + foundkV = true; + break; + } + } + } + if (foundkV) { + SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); + kV = kV_new; + } + } + return kV; + } + + /** + * Find the coupling ratio for all modules. + * + * @param swerveDrive {@link SwerveDrive} to operate with. + * @param volts Voltage to send to angle motors to spin. + * @param automatic Attempt to automatically spin the modules. + * @return Average coupling ratio. + */ + public static double findCouplingRatio(SwerveDrive swerveDrive, double volts, boolean automatic) { + System.out.println("Stopping the Swerve Drive."); + SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); + SwerveDriveTest.powerAngleMotorsVoltage(swerveDrive, 0); + Timer.delay(1); + double couplingRatioSum = 0; + for (SwerveModule module : swerveDrive.getModules()) { + if (module.getAbsoluteEncoder() == null) { + throw new RuntimeException("Absolute encoders are required to find the coupling ratio."); + } + SwerveAbsoluteEncoder absoluteEncoder = module.getAbsoluteEncoder(); + if (absoluteEncoder.readingError) { + throw new RuntimeException("Absolute encoder encountered a reading error please debug."); + } + System.out.println("Fetching the current absolute encoder and drive encoder position."); + module.getAngleMotor().setVoltage(0); + Timer.delay(1); + Rotation2d startingAbsoluteEncoderPosition = + Rotation2d.fromDegrees(absoluteEncoder.getAbsolutePosition()); + double driveEncoderPositionRotations = + module.getDriveMotor().getPosition() + / module.configuration.conversionFactors.drive.factor; + if (automatic) { + module.getAngleMotor().setVoltage(volts); + Timer.delay(0.01); + System.out.println("Rotating the module 360 degrees"); + while (!Rotation2d.fromDegrees(absoluteEncoder.getAbsolutePosition()) + .equals(startingAbsoluteEncoderPosition)) + ; + module.getAngleMotor().setVoltage(0); + } else { + DriverStation.reportWarning( + "Spin the " + + module.configuration.name + + " module 360 degrees now, you have 1 minute.\n", + false); + Timer.delay(60); + } + double couplingRatio = + (module.getDriveMotor().getPosition() + / module.configuration.conversionFactors.drive.factor) + - driveEncoderPositionRotations; + DriverStation.reportWarning( + module.configuration.name + " Coupling Ratio: " + couplingRatio, false); + couplingRatioSum += couplingRatio; + } + DriverStation.reportWarning("Average Coupling Ratio: " + (couplingRatioSum / 4.0), false); + return (couplingRatioSum / 4.0); + } + + /** + * Creates a SysIdRoutine.Config with a custom final timeout + * + * @param timeout - the most a SysIdRoutine should run + * @return A custom SysIdRoutine.Config + */ + public static Config createConfigCustomTimeout(double timeout) { + return new Config(null, null, Seconds.of(timeout)); + } + + /** + * Logs output, position and velocuty info form the drive motor to the SysIdRoutineLog
+ * Although SysIdRoutine expects to be logging Voltage, this function logs in Duty-Cycle (percent + * output) because it results in correctly adjusted values in the analysis for use in this + * library. + * + * @param module - the swerve module being logged + * @param log - the logger + */ + public static void logDriveMotorDutyCycle(SwerveModule module, SysIdRoutineLog log) { + logDriveMotorActivity( + module, + log, + () -> module.getDriveMotor().getVoltage() / RobotController.getBatteryVoltage()); + } + + /** + * Logs voltage, position and velocuty info form the drive motor to the SysIdRoutineLog + * + * @param module - the swerve module being logged + * @param log - the logger + */ + public static void logDriveMotorVoltage(SwerveModule module, SysIdRoutineLog log) { + logDriveMotorActivity(module, log, () -> module.getDriveMotor().getVoltage()); + } + + /** + * Logs power, position and velocuty info form the drive motor to the SysIdRoutineLog + * + * @param module - the swerve module being logged + * @param log - the logger + * @param powerSupplied - a functional supplier of the power to be logged + */ + public static void logDriveMotorActivity( + SwerveModule module, SysIdRoutineLog log, Supplier powerSupplied) { + double power = powerSupplied.get(); + double distance = module.getPosition().distanceMeters; + double velocity = module.getDriveMotor().getVelocity(); + SmartDashboard.putNumber( + "swerve/modules/" + module.configuration.name + "/SysId Drive Power", power); + SmartDashboard.putNumber( + "swerve/modules/" + module.configuration.name + "/SysId Drive Position", distance); + SmartDashboard.putNumber( + "swerve/modules/" + module.configuration.name + "/SysId Drive Velocity", velocity); + log.motor("drive-" + module.configuration.name) + .voltage(m_appliedVoltage.mut_replace(power, Volts)) + .linearPosition(m_distance.mut_replace(distance, Meters)) + .linearVelocity(m_velocity.mut_replace(velocity, MetersPerSecond)); + } + + /** + * Sets up the SysId runner and logger for the drive motors + * + * @param config - The SysIdRoutine.Config to use + * @param swerveSubsystem - the subsystem to add to requirements + * @param swerveDrive - the SwerveDrive from which to access motor info + * @param maxVolts - The maximum voltage that should be applied to the drive motors. + * @param testWithSpinning - Whether to make the robot spin in place instead of driving in a + * straight line, true to make the robot spin, false to make the robot drive in straight line + * @return A SysIdRoutine runner + */ + public static SysIdRoutine setDriveSysIdRoutine( + Config config, + SubsystemBase swerveSubsystem, + SwerveDrive swerveDrive, + double maxVolts, + boolean testWithSpinning) { + return new SysIdRoutine( + config, + new SysIdRoutine.Mechanism( + (Voltage voltage) -> { + if (!SwerveDriveTelemetry.isSimulation) { + if (testWithSpinning) { + SwerveDriveTest.setModulesToRotaryPosition(swerveDrive); + } else { + SwerveDriveTest.centerModules(swerveDrive); + } + SwerveDriveTest.powerDriveMotorsVoltage( + swerveDrive, Math.min(voltage.in(Volts), maxVolts)); + } else { + SwerveDriveTest.runDriveMotorsCharacterizationOnSimModules( + swerveDrive, voltage.in(Volts), testWithSpinning); + } + }, + log -> { + for (SwerveModule module : swerveDrive.getModules()) { + logDriveMotorVoltage(module, log); + } + }, + swerveSubsystem)); + } + + /** + * Logs info about the angle motor to the SysIdRoutineLog.
+ * Although SysIdRoutine expects to be logging Voltage, this function logs in Duty-Cycle (percent + * output) because it results in correctly adjusted values in the analysis for use in this + * library. + * + * @param module - the swerve module being logged + * @param log - the logger + */ + public static void logAngularMotorDutyCycle(SwerveModule module, SysIdRoutineLog log) { + logAngularMotorActivity( + module, + log, + () -> module.getAngleMotor().getVoltage() / RobotController.getBatteryVoltage()); + } + + /** + * Logs info about the angle motor to the SysIdRoutineLog + * + * @param module - the swerve module being logged + * @param log - the logger + */ + public static void logAngularMotorVoltage(SwerveModule module, SysIdRoutineLog log) { + logAngularMotorActivity(module, log, () -> module.getAngleMotor().getVoltage()); + } + + /** + * Logs info about the angle motor to the SysIdRoutineLog + * + * @param module - the swerve module being logged + * @param log - the logger + * @param powerSupplied - a functional supplier of the power to be logged + */ + public static void logAngularMotorActivity( + SwerveModule module, SysIdRoutineLog log, Supplier powerSupplied) { + double power = powerSupplied.get(); + double angle = module.getAngleMotor().getPosition(); + double velocity = module.getAngleMotor().getVelocity(); + SmartDashboard.putNumber( + "swerve/modules/" + module.configuration.name + "/SysId Angle Power", power); + SmartDashboard.putNumber( + "swerve/modules/" + module.configuration.name + "/SysId Angle Position", angle); + SmartDashboard.putNumber( + "swerve/modules/" + module.configuration.name + "/SysId Absolute Encoder Velocity", + velocity); + log.motor("angle-" + module.configuration.name) + .voltage(m_appliedVoltage.mut_replace(power, Volts)) + .angularPosition(m_anglePosition.mut_replace(angle, Degrees)) + .angularVelocity(m_angVelocity.mut_replace(velocity, DegreesPerSecond)); + } + + /** + * Sets up the SysId runner and logger for the angle motors + * + * @param config - The SysIdRoutine.Config to use + * @param swerveSubsystem - the subsystem to add to requirements + * @param swerveDrive - the SwerveDrive from which to access motor info + * @return A SysIdRoutineRunner + */ + public static SysIdRoutine setAngleSysIdRoutine( + Config config, SubsystemBase swerveSubsystem, SwerveDrive swerveDrive) { + return new SysIdRoutine( + config, + new SysIdRoutine.Mechanism( + (Voltage voltage) -> { + if (!SwerveDriveTelemetry.isSimulation) { + SwerveDriveTest.powerAngleMotorsVoltage(swerveDrive, voltage.in(Volts)); + SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); + } else { + SwerveDriveTest.runAngleMotorsCharacterizationOnSimModules( + swerveDrive, voltage.in(Volts)); + } + }, + log -> { + for (SwerveModule module : swerveDrive.getModules()) { + logAngularMotorVoltage(module, log); + } + }, + swerveSubsystem)); + } + + /** + * Creates a command that can be mapped to a button or other trigger. Delays can be set to + * customize the length of each part of the SysId Routine + * + * @param sysIdRoutine - The Sys Id routine runner + * @param delay - seconds between each portion to allow motors to spin down, etc... + * @param quasiTimeout - seconds to run the Quasistatic routines, so robot doesn't get too far + * @param dynamicTimeout - seconds to run the Dynamic routines, 2-3 secs should be enough + * @return A command that can be mapped to a button or other trigger + */ + public static Command generateSysIdCommand( + SysIdRoutine sysIdRoutine, double delay, double quasiTimeout, double dynamicTimeout) { + return sysIdRoutine + .quasistatic(SysIdRoutine.Direction.kForward) + .withTimeout(quasiTimeout) + .andThen(Commands.waitSeconds(delay)) + .andThen( + sysIdRoutine.quasistatic(SysIdRoutine.Direction.kReverse).withTimeout(quasiTimeout)) + .andThen(Commands.waitSeconds(delay)) + .andThen(sysIdRoutine.dynamic(SysIdRoutine.Direction.kForward).withTimeout(dynamicTimeout)) + .andThen(Commands.waitSeconds(delay)) + .andThen(sysIdRoutine.dynamic(SysIdRoutine.Direction.kReverse).withTimeout(dynamicTimeout)); + } +} diff --git a/src/main/java/swervelib/SwerveInputStream.java b/src/main/java/swervelib/SwerveInputStream.java new file mode 100644 index 00000000..7c848841 --- /dev/null +++ b/src/main/java/swervelib/SwerveInputStream.java @@ -0,0 +1,914 @@ +package swervelib; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.Nat; +import edu.wpi.first.math.Vector; +import edu.wpi.first.math.controller.ProfiledPIDController; +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.numbers.N2; +import edu.wpi.first.math.trajectory.TrapezoidProfile.State; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.DriverStation.Alliance; +import edu.wpi.first.wpilibj.XboxController; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import swervelib.math.SwerveMath; + +/** + * Helper class to easily transform Controller inputs into workable Chassis speeds. Intended to + * easily create an interface that generates {@link ChassisSpeeds} from {@link XboxController} + * + *


+ * Inspired by SciBorgs FRC 1155.
+ * Example: + * + *

{@code
+ * XboxController driverXbox = new XboxController(0);
+ *
+ * SwerveInputStream driveAngularVelocity = SwerveInputStream.of(drivebase.getSwerveDrive(),
+ *                                                               () -> driverXbox.getLeftY() * -1,
+ *                                                               () -> driverXbox.getLeftX() * -1) // Axis which give the desired translational angle and speed.
+ *                                                           .withControllerRotationAxis(driverXbox::getRightX) // Axis which give the desired angular velocity.
+ *                                                           .deadband(0.01)                  // Controller deadband
+ *                                                           .scaleTranslation(0.8)           // Scaled controller translation axis
+ *                                                           .allianceRelativeControl(true);  // Alliance relative controls.
+ *
+ * SwerveInputStream driveDirectAngle = driveAngularVelocity.copy()  // Copy the stream so further changes do not affect driveAngularVelocity
+ *                                                          .withControllerHeadingAxis(driverXbox::getRightX,
+ *                                                                                     driverXbox::getRightY) // Axis which give the desired heading angle using trigonometry.
+ *                                                          .headingWhile(true); // Enable heading based control.
+ * }
+ */ +public class SwerveInputStream implements Supplier { + + /** Translation suppliers. */ + private final DoubleSupplier controllerTranslationX; + /** Translational supplier. */ + private final DoubleSupplier controllerTranslationY; + /** {@link SwerveDrive} object for transformations. */ + private final SwerveDrive swerveDrive; + /** Rotation supplier as angular velocity. */ + private Optional controllerOmega = Optional.empty(); + /** Controller supplier as heading. */ + private Optional controllerHeadingX = Optional.empty(); + /** Controller supplier as heading. */ + private Optional controllerHeadingY = Optional.empty(); + /** Axis deadband for the controller. */ + private Optional axisDeadband = Optional.empty(); + /** Translational axis scalar value, should be between (0, 1]. */ + private Optional translationAxisScale = Optional.empty(); + /** Angular velocity axis scalar value, should be between (0, 1] */ + private Optional omegaAxisScale = Optional.empty(); + /** Target to aim at. */ + private Optional aimTarget = Optional.empty(); + /** Target {@link Supplier} to drive towards when driveToPose is enabled. */ + private Optional> driveToPose = Optional.empty(); + /** {@link ProfiledPIDController} for the translation while driving to a pose. Units are m/s */ + private Optional driveToPoseTranslationPIDController = Optional.empty(); + /** + * {@link ProfiledPIDController} for the Rotational axis while driving to a pose. Units are m/s + */ + private Optional driveToPoseOmegaPIDController = Optional.empty(); + /** Output {@link ChassisSpeeds} based on heading while this is True. */ + private Optional headingEnabled = Optional.empty(); + /** Locked heading for {@link SwerveInputMode#TRANSLATION_ONLY} */ + private Optional lockedHeading = Optional.empty(); + /** Output {@link ChassisSpeeds} based on aim while this is True. */ + private Optional aimEnabled = Optional.empty(); + /** Output {@link ChassisSpeeds} to move to a specific {@link Pose2d}. */ + private Optional driveToPoseEnabled = Optional.empty(); + /** Maintain current heading and drive without rotating, ideally. */ + private Optional translationOnlyEnabled = Optional.empty(); + /** Cube the translation magnitude from the controller. */ + private Optional translationCube = Optional.empty(); + /** Cube the angular velocity axis from the controller. */ + private Optional omegaCube = Optional.empty(); + /** Robot relative oriented output expected. */ + private Optional robotRelative = Optional.empty(); + /** Field oriented chassis output is relative to your current alliance. */ + private Optional allianceRelative = Optional.empty(); + /** Heading offset enable state. */ + private Optional translationHeadingOffsetEnabled = Optional.empty(); + /** Heading offset to apply during heading based control. */ + private Optional translationHeadingOffset = Optional.empty(); + /** {@link SwerveController} for simple control over heading. */ + private SwerveController swerveController = null; + /** Current {@link SwerveInputMode} to use. */ + private SwerveInputMode currentMode = SwerveInputMode.ANGULAR_VELOCITY; + + /** + * Create a {@link SwerveInputStream} for an easy way to generate {@link ChassisSpeeds} from a + * driver controller. + * + * @param drive {@link SwerveDrive} object for transformation. + * @param x Translation X input in range of [-1, 1] + * @param y Translation Y input in range of [-1, 1] + */ + private SwerveInputStream(SwerveDrive drive, DoubleSupplier x, DoubleSupplier y) { + controllerTranslationX = x; + controllerTranslationY = y; + swerveDrive = drive; + } + + /** + * Create a {@link SwerveInputStream} for an easy way to generate {@link ChassisSpeeds} from a + * driver controller. + * + * @param drive {@link SwerveDrive} object for transformation. + * @param x Translation X input in range of [-1, 1] + * @param y Translation Y input in range of [-1, 1] + * @param rot Rotation input in range of [-1, 1] + */ + public SwerveInputStream( + SwerveDrive drive, DoubleSupplier x, DoubleSupplier y, DoubleSupplier rot) { + this(drive, x, y); + controllerOmega = Optional.of(rot); + } + + /** + * Create a {@link SwerveInputStream} for an easy way to generate {@link ChassisSpeeds} from a + * driver controller. + * + * @param drive {@link SwerveDrive} object for transformation. + * @param x Translation X input in range of [-1, 1] + * @param y Translation Y input in range of [-1, 1] + * @param headingX Heading X input in range of [-1, 1] + * @param headingY Heading Y input in range of [-1, 1] + */ + public SwerveInputStream( + SwerveDrive drive, + DoubleSupplier x, + DoubleSupplier y, + DoubleSupplier headingX, + DoubleSupplier headingY) { + this(drive, x, y); + controllerHeadingX = Optional.of(headingX); + controllerHeadingY = Optional.of(headingY); + } + + /** + * Create basic {@link SwerveInputStream} without any rotation components. + * + * @param drive {@link SwerveDrive} object for transformation. + * @param x {@link DoubleSupplier} of the translation X axis of the controller joystick to use. + * @param y {@link DoubleSupplier} of the translation X axis of the controller joystick to use. + * @return {@link SwerveInputStream} to use as you see fit. + */ + public static SwerveInputStream of(SwerveDrive drive, DoubleSupplier x, DoubleSupplier y) { + return new SwerveInputStream(drive, x, y); + } + + /** + * Copy the {@link SwerveInputStream} object. + * + * @return Clone of current {@link SwerveInputStream} + */ + public SwerveInputStream copy() { + SwerveInputStream newStream = + new SwerveInputStream(swerveDrive, controllerTranslationX, controllerTranslationY); + newStream.controllerOmega = controllerOmega; + newStream.controllerHeadingX = controllerHeadingX; + newStream.controllerHeadingY = controllerHeadingY; + newStream.axisDeadband = axisDeadband; + newStream.translationAxisScale = translationAxisScale; + newStream.omegaAxisScale = omegaAxisScale; + newStream.driveToPose = driveToPose; + newStream.driveToPoseTranslationPIDController = driveToPoseTranslationPIDController; + newStream.driveToPoseOmegaPIDController = driveToPoseOmegaPIDController; + newStream.aimTarget = aimTarget; + newStream.headingEnabled = headingEnabled; + newStream.aimEnabled = aimEnabled; + newStream.driveToPoseEnabled = driveToPoseEnabled; + newStream.currentMode = currentMode; + newStream.translationOnlyEnabled = translationOnlyEnabled; + newStream.lockedHeading = lockedHeading; + newStream.swerveController = swerveController; + newStream.omegaCube = omegaCube; + newStream.translationCube = translationCube; + newStream.robotRelative = robotRelative; + newStream.allianceRelative = allianceRelative; + newStream.translationHeadingOffsetEnabled = translationHeadingOffsetEnabled; + newStream.translationHeadingOffset = translationHeadingOffset; + return newStream; + } + + /** + * Set the stream to output robot relative {@link ChassisSpeeds} + * + * @param enabled Robot-Relative {@link ChassisSpeeds} output. + * @return self + */ + public SwerveInputStream robotRelative(BooleanSupplier enabled) { + robotRelative = Optional.of(enabled); + return this; + } + + /** + * Set the stream to output robot relative {@link ChassisSpeeds} + * + * @param enabled Robot-Relative {@link ChassisSpeeds} output. + * @return self + */ + public SwerveInputStream robotRelative(boolean enabled) { + robotRelative = enabled ? Optional.of(() -> enabled) : Optional.empty(); + return this; + } + + /** + * Drive to a given pose with the provided {@link ProfiledPIDController}s + * + * @param pose {@link Supplier} for ease of use. + * @param xPIDController PID controller for the translational axis, units are m/s. + * @param omegaPIDController PID Controller for rotational axis, units are rad/s. + * @return self + */ + public SwerveInputStream driveToPose( + Supplier pose, + ProfiledPIDController xPIDController, + ProfiledPIDController omegaPIDController) { + omegaPIDController.reset(swerveDrive.getPose().getRotation().getRadians()); + xPIDController.reset( + swerveDrive.getPose().getTranslation().getDistance(pose.get().getTranslation())); + omegaPIDController.enableContinuousInput(-Math.PI, Math.PI); + xPIDController.setGoal(new State(0, 0)); + driveToPose = Optional.of(pose); + driveToPoseTranslationPIDController = Optional.of(xPIDController); + driveToPoseOmegaPIDController = Optional.of(omegaPIDController); + return this; + } + + /** + * Enable driving to the target pose. + * + * @param enabled Enable state of drive to pose. + * @return self. + */ + public SwerveInputStream driveToPoseEnabled(BooleanSupplier enabled) { + driveToPoseEnabled = Optional.of(enabled); + return this; + } + + /** + * Enable driving to the target pose. + * + * @param enabled Enable state of drive to pose. + * @return self. + */ + public SwerveInputStream driveToPoseEnabled(boolean enabled) { + driveToPoseEnabled = enabled ? Optional.of(() -> enabled) : Optional.empty(); + Pose2d swervePose = swerveDrive.getPose(); + // driveToPoseXPIDController.ifPresent(profiledPIDController -> + // profiledPIDController.reset(swervePose.getX())); + // driveToPoseYPIDController.ifPresent(profiledPIDController -> + // profiledPIDController.reset(swervePose.getY())); + // driveToPoseOmegaPIDController.ifPresent(profiledPIDController -> + // profiledPIDController.reset(swervePose.getRotation() + // + // .getRadians())); + return this; + } + + /** + * Heading offset enabled boolean supplier. + * + * @param enabled Enable state + * @return self + */ + public SwerveInputStream translationHeadingOffset(BooleanSupplier enabled) { + translationHeadingOffsetEnabled = Optional.of(enabled); + return this; + } + + /** + * Heading offset enable + * + * @param enabled Enable state + * @return self + */ + public SwerveInputStream translationHeadingOffset(boolean enabled) { + translationHeadingOffsetEnabled = enabled ? Optional.of(() -> enabled) : Optional.empty(); + return this; + } + + /** + * Set the heading offset angle. + * + * @param angle {@link Rotation2d} offset to apply + * @return self + */ + public SwerveInputStream translationHeadingOffset(Rotation2d angle) { + translationHeadingOffset = Optional.of(angle); + return this; + } + + /** + * Modify the output {@link ChassisSpeeds} so that it is always relative to your alliance. + * + * @param enabled Alliance aware {@link ChassisSpeeds} output. + * @return self + */ + public SwerveInputStream allianceRelativeControl(BooleanSupplier enabled) { + allianceRelative = Optional.of(enabled); + return this; + } + + /** + * Modify the output {@link ChassisSpeeds} so that it is always relative to your alliance. + * + * @param enabled Alliance aware {@link ChassisSpeeds} output. + * @return self + */ + public SwerveInputStream allianceRelativeControl(boolean enabled) { + allianceRelative = enabled ? Optional.of(() -> enabled) : Optional.empty(); + return this; + } + + /** + * Cube the angular velocity controller axis for a non-linear controls scheme. + * + * @param enabled Enabled state for the stream. + * @return self. + */ + public SwerveInputStream cubeRotationControllerAxis(BooleanSupplier enabled) { + omegaCube = Optional.of(enabled); + return this; + } + + /** + * Cube the angular velocity controller axis for a non-linear controls scheme. + * + * @param enabled Enabled state for the stream. + * @return self. + */ + public SwerveInputStream cubeRotationControllerAxis(boolean enabled) { + omegaCube = Optional.of(() -> enabled); + return this; + } + + /** + * Cube the translation axis magnitude for a non-linear control scheme. + * + * @param enabled Enabled state for the stream + * @return self + */ + public SwerveInputStream cubeTranslationControllerAxis(BooleanSupplier enabled) { + translationOnlyEnabled = Optional.of(enabled); + return this; + } + + /** + * Cube the translation axis magnitude for a non-linear control scheme + * + * @param enabled Enabled state for the stream + * @return self + */ + public SwerveInputStream cubeTranslationControllerAxis(boolean enabled) { + translationCube = enabled ? Optional.of(() -> enabled) : Optional.empty(); + return this; + } + + /** + * Add a rotation axis for Angular Velocity control + * + * @param rot Rotation axis with values from [-1, 1] + * @return self + */ + public SwerveInputStream withControllerRotationAxis(DoubleSupplier rot) { + controllerOmega = Optional.of(rot); + return this; + } + + /** + * Add heading axis for Heading based control. + * + * @param headingX Heading X axis with values from [-1, 1] + * @param headingY Heading Y axis with values from [-1, 1] + * @return self + */ + public SwerveInputStream withControllerHeadingAxis( + DoubleSupplier headingX, DoubleSupplier headingY) { + controllerHeadingX = Optional.of(headingX); + controllerHeadingY = Optional.of(headingY); + return this; + } + + /** + * Set a deadband for all controller axis. + * + * @param deadband Deadband to set, should be between [0, 1) + * @return self + */ + public SwerveInputStream deadband(double deadband) { + axisDeadband = deadband == 0 ? Optional.empty() : Optional.of(deadband); + return this; + } + + /** + * Scale the translation axis for {@link SwerveInputStream} by a constant scalar value. + * + * @param scaleTranslation Translation axis scalar value. (0, 1] + * @return this + */ + public SwerveInputStream scaleTranslation(double scaleTranslation) { + translationAxisScale = scaleTranslation == 0 ? Optional.empty() : Optional.of(scaleTranslation); + return this; + } + + /** + * Scale the rotation axis input for {@link SwerveInputStream} to reduce the range in which they + * operate. + * + * @param scaleRotation Angular velocity axis scalar value. (0, 1] + * @return this + */ + public SwerveInputStream scaleRotation(double scaleRotation) { + omegaAxisScale = scaleRotation == 0 ? Optional.empty() : Optional.of(scaleRotation); + return this; + } + + /** + * Output {@link ChassisSpeeds} based on heading while the supplier is True. + * + * @param trigger Supplier to use. + * @return this. + */ + public SwerveInputStream headingWhile(BooleanSupplier trigger) { + headingEnabled = Optional.of(trigger); + return this; + } + + /** + * Set the heading enable state. + * + * @param headingState Heading enabled state. + * @return this + */ + public SwerveInputStream headingWhile(boolean headingState) { + if (headingState) { + headingEnabled = Optional.of(() -> true); + } else { + headingEnabled = Optional.empty(); + } + return this; + } + + /** + * Aim the {@link SwerveDrive} at this pose while driving. + * + * @param aimTarget {@link Pose2d} to point at. + * @return this + */ + public SwerveInputStream aim(Pose2d aimTarget) { + this.aimTarget = aimTarget.equals(Pose2d.kZero) ? Optional.empty() : Optional.of(aimTarget); + return this; + } + + /** + * Enable aiming while the trigger is true. + * + * @param trigger When True will enable aiming at the current target. + * @return this. + */ + public SwerveInputStream aimWhile(BooleanSupplier trigger) { + aimEnabled = Optional.of(trigger); + return this; + } + + /** + * Enable aiming while the trigger is true. + * + * @param trigger When True will enable aiming at the current target. + * @return this. + */ + public SwerveInputStream aimWhile(boolean trigger) { + if (trigger) { + aimEnabled = Optional.of(() -> true); + } else { + aimEnabled = Optional.empty(); + } + return this; + } + + /** + * Enable locking of rotation and only translating, overrides everything. + * + * @param trigger Translation only while returns true. + * @return this + */ + public SwerveInputStream translationOnlyWhile(BooleanSupplier trigger) { + translationOnlyEnabled = Optional.of(trigger); + return this; + } + + /** + * Enable locking of rotation and only translating, overrides everything. + * + * @param translationState Translation only if true. + * @return this + */ + public SwerveInputStream translationOnlyWhile(boolean translationState) { + if (translationState) { + translationOnlyEnabled = Optional.of(() -> true); + } else { + translationOnlyEnabled = Optional.empty(); + } + return this; + } + + /** + * Find {@link SwerveInputMode} based off existing parameters of the {@link SwerveInputStream} + * + * @return The calculated {@link SwerveInputMode}, defaults to {@link + * SwerveInputMode#ANGULAR_VELOCITY}. + */ + private SwerveInputMode findMode() { + if (driveToPoseEnabled.isPresent() && driveToPoseEnabled.get().getAsBoolean()) { + if (driveToPose.isPresent()) { + if (driveToPoseOmegaPIDController.isPresent() + && driveToPoseTranslationPIDController.isPresent()) { + return SwerveInputMode.DRIVE_TO_POSE; + } + System.out.println("Drive to pose present"); + DriverStation.reportError("Drive to pose not supplied with pid controllers.", false); + } + DriverStation.reportError("Drive to pose enabled without supplier present.", false); + } else if (translationOnlyEnabled.isPresent() && translationOnlyEnabled.get().getAsBoolean()) { + return SwerveInputMode.TRANSLATION_ONLY; + } else if (aimEnabled.isPresent() && aimEnabled.get().getAsBoolean()) { + if (aimTarget.isPresent()) { + return SwerveInputMode.AIM; + } else { + DriverStation.reportError( + "Attempting to enter AIM mode without target, please use SwerveInputStream.aim() to select a target first!", + false); + } + } else if (headingEnabled.isPresent() && headingEnabled.get().getAsBoolean()) { + if (controllerHeadingX.isPresent() && controllerHeadingY.isPresent()) { + return SwerveInputMode.HEADING; + } else { + DriverStation.reportError( + "Attempting to enter HEADING mode without heading axis, please use SwerveInputStream.withControllerHeadingAxis to add heading axis!", + false); + } + } else if (controllerOmega.isEmpty()) { + DriverStation.reportError( + "Attempting to enter ANGULAR_VELOCITY mode without a rotation axis, please use SwerveInputStream.withControllerRotationAxis to add angular velocity axis!", + false); + return SwerveInputMode.TRANSLATION_ONLY; + } + return SwerveInputMode.ANGULAR_VELOCITY; + } + + /** + * Transition smoothly from one mode to another. + * + * @param newMode New mode to transition too. + */ + private void transitionMode(SwerveInputMode newMode) { + // Handle removing of current mode. + switch (currentMode) { + case TRANSLATION_ONLY -> { + lockedHeading = Optional.empty(); + break; + } + case ANGULAR_VELOCITY, HEADING, AIM -> { + // Do nothing + break; + } + case DRIVE_TO_POSE -> { + break; + } + } + + // Transitioning to new mode + switch (newMode) { + case TRANSLATION_ONLY -> { + lockedHeading = Optional.of(swerveDrive.getOdometryHeading()); + break; + } + case ANGULAR_VELOCITY -> { + if (swerveDrive.headingCorrection) { + swerveDrive.setHeadingCorrection(false); + } + break; + } + case HEADING, AIM -> { + // Do nothing + break; + } + case DRIVE_TO_POSE -> { + if (swerveDrive.headingCorrection) { + swerveDrive.setHeadingCorrection(false); + } + } + } + } + + /** + * Apply the deadband if it exists. + * + * @param axisValue Axis value to apply the deadband too. + * @return axis value with deadband, else axis value straight. + */ + private double applyDeadband(double axisValue) { + if (axisDeadband.isPresent()) { + return MathUtil.applyDeadband(axisValue, axisDeadband.get()); + } + return axisValue; + } + + /** + * Apply the scalar value if it exists. + * + * @param axisValue Axis value to apply teh scalar too. + * @return Axis value scaled by scalar value. + */ + private double applyRotationalScalar(double axisValue) { + if (omegaAxisScale.isPresent()) { + return axisValue * omegaAxisScale.get(); + } + return axisValue; + } + + /** + * Scale the translational axis by the {@link SwerveInputStream#translationAxisScale} if it + * exists. + * + * @param xAxis X axis to scale. + * @param yAxis Y axis to scale. + * @return Scaled {@link Translation2d} + */ + private Translation2d applyTranslationScalar(double xAxis, double yAxis) { + if (translationAxisScale.isPresent()) { + + return SwerveMath.scaleTranslation( + new Translation2d(xAxis, yAxis), translationAxisScale.get()); + } + return new Translation2d(xAxis, yAxis); + } + + /** + * Apply the cube transformation on the given {@link Translation2d} + * + * @param translation {@link Translation2d} representing controller input + * @return Cubed {@link Translation2d} if the {@link SwerveInputStream#translationCube} is + * present. + */ + private Translation2d applyTranslationCube(Translation2d translation) { + if (translationCube.isPresent() && translationCube.get().getAsBoolean()) { + return SwerveMath.cubeTranslation(translation); + } + return translation; + } + + /** + * Apply the cube transformation on the given rotation controller axis + * + * @param rotationAxis Rotation controller axis to cube. + * @return Cubed axis value if the {@link SwerveInputStream#omegaCube} is present. + */ + private double applyOmegaCube(double rotationAxis) { + if (omegaCube.isPresent() && omegaCube.get().getAsBoolean()) { + return Math.pow(rotationAxis, 3); + } + return rotationAxis; + } + + /** + * Change {@link ChassisSpeeds} from robot relative if enabled. + * + * @param fieldRelativeSpeeds Field or robot relative speeds to translate into robot-relative + * speeds. + * @return Field relative {@link ChassisSpeeds}. + */ + private ChassisSpeeds applyRobotRelativeTranslation(ChassisSpeeds fieldRelativeSpeeds) { + if (robotRelative.isPresent() && robotRelative.get().getAsBoolean()) { + return ChassisSpeeds.fromRobotRelativeSpeeds( + fieldRelativeSpeeds, swerveDrive.getOdometryHeading()); + } + return fieldRelativeSpeeds; + } + + /** + * Apply alliance aware translation which flips the {@link Translation2d} if the robot is on the + * Blue alliance. + * + * @param fieldRelativeTranslation Field-relative {@link Translation2d} to flip. + * @return Alliance-oriented {@link Translation2d} + */ + private Translation2d applyAllianceAwareTranslation(Translation2d fieldRelativeTranslation) { + if (allianceRelative.isPresent() && allianceRelative.get().getAsBoolean()) { + if (robotRelative.isPresent() && robotRelative.get().getAsBoolean()) { + if (driveToPoseEnabled.isPresent() && driveToPoseEnabled.get().getAsBoolean()) { + return fieldRelativeTranslation; + } + throw new RuntimeException( + "Cannot use robot oriented control with Alliance aware movement!"); + } + if (DriverStation.getAlliance().isPresent() + && DriverStation.getAlliance().get() == Alliance.Red) { + return fieldRelativeTranslation.rotateBy(Rotation2d.k180deg); + } + } + return fieldRelativeTranslation; + } + + /** + * Adds offset to translation if one is set. + * + * @param speeds {@link ChassisSpeeds} to offset + * @return Offsetted {@link ChassisSpeeds} + */ + private ChassisSpeeds applyTranslationHeadingOffset(ChassisSpeeds speeds) { + if (translationHeadingOffsetEnabled.isPresent() + && translationHeadingOffsetEnabled.get().getAsBoolean()) { + if (translationHeadingOffset.isPresent()) { + Translation2d speedsTranslation = + new Translation2d(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond) + .rotateBy(translationHeadingOffset.get()); + return new ChassisSpeeds( + speedsTranslation.getX(), speedsTranslation.getY(), speeds.omegaRadiansPerSecond); + } + } + return speeds; + } + + /** + * When the {@link SwerveInputStream} is in {@link SwerveInputMode#DRIVE_TO_POSE} this function + * will return if the robot is at the desired pose within the defined tolerance. + * + * @param toleranceMeters Tolerance in meters. + * @return At target pose, true if current mode is not {@link SwerveInputMode#DRIVE_TO_POSE} and + * no pose supplier has been given. + */ + public boolean atTargetPose(double toleranceMeters) { + if (currentMode != SwerveInputMode.DRIVE_TO_POSE) { + DriverStation.reportError( + "SwerveInputStream.atTargetPose called while not set to DriveToPose.", false); + if (!driveToPose.isPresent()) { + return true; + } + } + if (driveToPose.isPresent()) { + Pose2d targetPose = driveToPose.get().get(); + return swerveDrive.getPose().getTranslation().getDistance(targetPose.getTranslation()) + <= toleranceMeters; + } + return true; + } + + /** + * Gets a {@link ChassisSpeeds} + * + * @return {@link ChassisSpeeds} + */ + @Override + public ChassisSpeeds get() { + double maximumChassisVelocity = swerveDrive.getMaximumChassisVelocity(); + Translation2d scaledTranslation = + applyTranslationScalar( + applyDeadband(controllerTranslationX.getAsDouble()), + applyDeadband(controllerTranslationY.getAsDouble())); + scaledTranslation = applyTranslationCube(scaledTranslation); + scaledTranslation = applyAllianceAwareTranslation(scaledTranslation); + + double vxMetersPerSecond = scaledTranslation.getX() * maximumChassisVelocity; + double vyMetersPerSecond = scaledTranslation.getY() * maximumChassisVelocity; + double omegaRadiansPerSecond = 0; + ChassisSpeeds speeds = new ChassisSpeeds(); + + SwerveInputMode newMode = findMode(); + // Handle transitions here. + if (currentMode != newMode) { + transitionMode(newMode); + } + if (swerveController == null) { + swerveController = swerveDrive.getSwerveController(); + } + switch (newMode) { + case TRANSLATION_ONLY -> { + omegaRadiansPerSecond = + swerveController.headingCalculate( + swerveDrive.getOdometryHeading().getRadians(), lockedHeading.get().getRadians()); + speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); + break; + } + case ANGULAR_VELOCITY -> { + omegaRadiansPerSecond = + applyOmegaCube( + applyRotationalScalar(applyDeadband(controllerOmega.get().getAsDouble()))) + * swerveDrive.getMaximumChassisAngularVelocity(); + speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); + break; + } + case HEADING -> { + omegaRadiansPerSecond = + swerveController.headingCalculate( + swerveDrive.getOdometryHeading().getRadians(), + Rotation2d.fromRadians( + swerveController.getJoystickAngle( + controllerHeadingX.get().getAsDouble(), + controllerHeadingY.get().getAsDouble())) + .getRadians()); + + // Prevent rotation if controller heading inputs are not past axisDeadband + if (Math.abs(controllerHeadingX.get().getAsDouble()) + + Math.abs(controllerHeadingY.get().getAsDouble()) + < axisDeadband.get()) { + omegaRadiansPerSecond = 0; + } + speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); + break; + } + case AIM -> { + Rotation2d currentHeading = swerveDrive.getOdometryHeading(); + Translation2d relativeTrl = + aimTarget.get().relativeTo(swerveDrive.getPose()).getTranslation(); + Rotation2d target = + new Rotation2d(relativeTrl.getX(), relativeTrl.getY()).plus(currentHeading); + omegaRadiansPerSecond = + swerveController.headingCalculate(currentHeading.getRadians(), target.getRadians()); + speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); + break; + } + case DRIVE_TO_POSE -> { + // Written by team 8865! + ProfiledPIDController translationPIDController = driveToPoseTranslationPIDController.get(); + ProfiledPIDController rotationPIDController = driveToPoseOmegaPIDController.get(); + Pose2d swervePoseSetpoint = driveToPose.get().get(); + Pose2d robotPose = swerveDrive.getPose(); + Vector robotVec = robotPose.getTranslation().toVector(); + Vector targetPoseRelativeToRobotPose = + swervePoseSetpoint.getTranslation().toVector().minus(robotVec); + double distanceFromTarget = targetPoseRelativeToRobotPose.norm(); + + Vector traversalVector = new Vector(Nat.N2()); + traversalVector.set(0, 0, targetPoseRelativeToRobotPose.get(0, 0)); + traversalVector.set(1, 0, targetPoseRelativeToRobotPose.get(1, 0)); + traversalVector = + traversalVector + .unit() + .times(-translationPIDController.calculate(distanceFromTarget, 0)); + + Vector robotForwardVec = + robotPose + .transformBy(new Transform2d(1, 0, new Rotation2d())) + .getTranslation() + .toVector() + .minus(robotVec); + Vector robotLateralVec = + robotPose + .transformBy(new Transform2d(0, 1, new Rotation2d())) + .getTranslation() + .toVector() + .minus(robotVec); + + currentMode = newMode; + speeds = + ChassisSpeeds.fromRobotRelativeSpeeds( + new ChassisSpeeds( + robotForwardVec.norm() * traversalVector.dot(robotForwardVec), + robotLateralVec.norm() * traversalVector.dot(robotLateralVec), + rotationPIDController.calculate( + robotPose.getRotation().getRadians(), + swervePoseSetpoint.getRotation().getRadians())), + swerveDrive.getOdometryHeading()); + double lerpDistance = + robotPose + .getTranslation() + .plus(new Translation2d(speeds.vxMetersPerSecond, vyMetersPerSecond).times(0.02)) + .getDistance(swervePoseSetpoint.getTranslation()); + // Filter out incorrect ChassisSpeeds. + if (lerpDistance > distanceFromTarget) { + speeds = new ChassisSpeeds(0, 0, 0); + } + + return speeds; + } + } + + currentMode = newMode; + + return applyTranslationHeadingOffset(applyRobotRelativeTranslation(speeds)); + } + + /** Drive modes to keep track of. */ + enum SwerveInputMode { + /** Translation only mode, does not allow for rotation and maintains current heading. */ + TRANSLATION_ONLY, + /** Output based off angular velocity */ + ANGULAR_VELOCITY, + /** Output based off of heading. */ + HEADING, + /** Output based off of targeting. */ + AIM, + /** Drive to a target pose. */ + DRIVE_TO_POSE + } +} diff --git a/src/main/java/swervelib/SwerveModule.java b/src/main/java/swervelib/SwerveModule.java new file mode 100644 index 00000000..de367512 --- /dev/null +++ b/src/main/java/swervelib/SwerveModule.java @@ -0,0 +1,890 @@ +package swervelib; + +import static edu.wpi.first.units.Units.InchesPerSecond; +import static edu.wpi.first.units.Units.MetersPerSecond; +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static edu.wpi.first.units.Units.RotationsPerSecond; + +import edu.wpi.first.math.controller.SimpleMotorFeedforward; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.networktables.BooleanPublisher; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.LinearVelocity; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import swervelib.encoders.SparkMaxEncoderSwerve; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.math.SwerveMath; +import swervelib.motors.SparkMaxBrushedMotorSwerve; +import swervelib.motors.SparkMaxSwerve; +import swervelib.motors.SwerveMotor; +import swervelib.parser.Cache; +import swervelib.parser.PIDFConfig; +import swervelib.parser.SwerveModuleConfiguration; +import swervelib.parser.SwerveModulePhysicalCharacteristics; +import swervelib.simulation.SwerveModuleSimulation; +import swervelib.telemetry.SwerveDriveTelemetry; +import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; + +/** The Swerve Module class which represents and controls Swerve Modules for the swerve drive. */ +public class SwerveModule implements AutoCloseable { + + /** Swerve module configuration options. */ + public final SwerveModuleConfiguration configuration; + /** Absolute encoder position cache. */ + public final Cache absolutePositionCache; + /** Drive motor position cache. */ + public final Cache drivePositionCache; + /** Drive motor velocity cache. */ + public final Cache driveVelocityCache; + /** + * Module number for kinematics, usually 0 to 3. front left -> front right -> back left -> back + * right. + */ + public final int moduleNumber; + /** Swerve Motors. */ + private final SwerveMotor angleMotor, driveMotor; + /** Absolute encoder for swerve drive. */ + private final SwerveAbsoluteEncoder absoluteEncoder; + /** An {@link Alert} for if pushing the Absolute Encoder offset to the encoder fails. */ + private final Alert encoderOffsetWarning; + /** An {@link Alert} for if there is no Absolute Encoder on the module. */ + private final Alert noEncoderWarning; + /** An {@link Alert} for if there is no Absolute Encoder on the module. */ + private final Alert externalSensorIsNull = + new Alert("No absolute Encoder found.", AlertType.kError); + /** An {@link Alert} for if the offset is 0 degrees. */ + private final Alert internalOffsetIsZero = + new Alert("Absolute encoder offset is 0, this may be a problem.", AlertType.kWarning); + /** + * An {@link Alert} for if the angle/steer/azimuth motor is incompatible with the absolute + * encoder. + */ + private final Alert externalFeedbackIncompatible = + new Alert( + "Absolute encoder is incompatible, cannot set as an external feedback device.", + AlertType.kError); + /** An {@link Alert} for if the absolute encoder cannot set an offset. */ + private final Alert externalOffsetIncompatible = + new Alert( + "Absolute encoder is incompatible, cannot set an offset internally.", AlertType.kError); + /** NT4 Raw Absolute Angle publisher for the absolute encoder. */ + private final DoublePublisher rawAbsoluteAnglePublisher; + /** NT4 Adjusted Absolute angle publisher for the absolute encoder. */ + private final DoublePublisher adjAbsoluteAnglePublisher; + /** NT4 Absolute encoder read issue. */ + private final BooleanPublisher absoluteEncoderIssuePublisher; + /** NT4 raw angle motor. */ + private final DoublePublisher rawAnglePublisher; + /** NT4 Raw drive motor. */ + private final DoublePublisher rawDriveEncoderPublisher; + /** NT4 Raw drive motor. */ + private final DoublePublisher rawDriveVelocityPublisher; + /** Speed setpoint publisher for the module motor-controller PID. */ + private final DoublePublisher speedSetpointPublisher; + /** Angle setpoint publisher for the module motor-controller PID. */ + private final DoublePublisher angleSetpointPublisher; + /** Maximum {@link LinearVelocity} for the drive motor of the swerve module. */ + private LinearVelocity maxDriveVelocity; + /** Maximum velocity for the drive motor of the swerve module. */ + private double maxDriveVelocityMetersPerSecond; + /** Maximum {@link AngularVelocity} for the azimuth/angle motor of the swerve module. */ + private AngularVelocity maxAngularVelocity; + /** Feedforward for the drive motor during closed loop control. */ + private SimpleMotorFeedforward driveMotorFeedforward; + /** Anti-Jitter AKA auto-centering disabled. */ + private boolean antiJitterEnabled = true; + /** Last swerve module state applied. */ + private SwerveModuleState lastState; + /** Angle offset from the absolute encoder. */ + private double angleOffset; + /** Simulated swerve module. */ + private SwerveModuleSimulation simModule; + /** Enables utilization off {@link SwerveModuleState#optimize(Rotation2d)} */ + private boolean optimizeSwerveModuleState = true; + /** Encoder synchronization queued. */ + private boolean synchronizeEncoderQueued = false; + /** Encoder, Absolute encoder synchronization enabled. */ + private boolean synchronizeEncoderEnabled = false; + /** Encoder synchronization deadband in degrees. */ + private double synchronizeEncoderDeadband = 3; + + /** + * Construct the swerve module and initialize the swerve module motors and absolute encoder. + * + * @param moduleNumber Module number for kinematics. + * @param moduleConfiguration Module constants containing CAN ID's and offsets. + */ + public SwerveModule(int moduleNumber, SwerveModuleConfiguration moduleConfiguration) { + // angle = 0; + // speed = 0; + // omega = 0; + // fakePos = 0; + this.moduleNumber = moduleNumber; + configuration = moduleConfiguration; + angleOffset = moduleConfiguration.angleOffset; + + // Create motors from configuration and reset them to defaults. + angleMotor = moduleConfiguration.angleMotor; + driveMotor = moduleConfiguration.driveMotor; + angleMotor.factoryDefaults(); + driveMotor.factoryDefaults(); + + // Initialize Feedforwards. + driveMotorFeedforward = getDefaultFeedforward(); + + // Configure voltage comp, current limit, and ramp rate. + angleMotor.setVoltageCompensation(configuration.physicalCharacteristics.optimalVoltage); + driveMotor.setVoltageCompensation(configuration.physicalCharacteristics.optimalVoltage); + angleMotor.setCurrentLimit(configuration.physicalCharacteristics.angleMotorCurrentLimit); + driveMotor.setCurrentLimit(configuration.physicalCharacteristics.driveMotorCurrentLimit); + angleMotor.setLoopRampRate(configuration.physicalCharacteristics.angleMotorRampRate); + driveMotor.setLoopRampRate(configuration.physicalCharacteristics.driveMotorRampRate); + + // Config angle encoders + absoluteEncoder = moduleConfiguration.absoluteEncoder; + if (absoluteEncoder != null) { + absoluteEncoder.factoryDefault(); + absoluteEncoder.configure(moduleConfiguration.absoluteEncoderInverted); + } + + if (SwerveDriveTelemetry.isSimulation) { + simModule = new SwerveModuleSimulation(); + } + + // Setup the cache for the absolute encoder position. + absolutePositionCache = new Cache<>(this::getRawAbsolutePosition, 20); + + // Config angle motor/controller + if (!angleMotor.usingExternalFeedbackSensor()) { + angleMotor.configureIntegratedEncoder(moduleConfiguration.conversionFactors.angle.factor); + } + angleMotor.configurePIDF(moduleConfiguration.anglePIDF); + angleMotor.configurePIDWrapping(0, 360); + angleMotor.setInverted(moduleConfiguration.angleMotorInverted); + angleMotor.setMotorBrake(false); + + // Set the position AFTER settings the conversion factor. + if (absoluteEncoder != null) { + angleMotor.setPosition(getAbsolutePosition()); + } + + // Config drive motor/controller + driveMotor.configureIntegratedEncoder(moduleConfiguration.conversionFactors.drive.factor); + driveMotor.configurePIDF(moduleConfiguration.velocityPIDF); + driveMotor.setInverted(moduleConfiguration.driveMotorInverted); + driveMotor.setMotorBrake(true); + + driveMotor.burnFlash(); + angleMotor.burnFlash(); + + drivePositionCache = new Cache<>(driveMotor::getPosition, 20); + driveVelocityCache = new Cache<>(driveMotor::getVelocity, 20); + + // Force a cache update on init. + driveVelocityCache.update(); + drivePositionCache.update(); + absolutePositionCache.update(); + + // Save the current state. + lastState = getState(); + + noEncoderWarning = + new Alert( + "Motors", + "There is no Absolute Encoder on module #" + moduleNumber, + AlertType.kWarning); + encoderOffsetWarning = + new Alert( + "Motors", + "Pushing the Absolute Encoder offset to the encoder failed on module #" + moduleNumber, + AlertType.kWarning); + + rawAbsoluteAnglePublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Absolute Encoder") + .publish(); + adjAbsoluteAnglePublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Adjusted Absolute Encoder") + .publish(); + absoluteEncoderIssuePublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getBooleanTopic( + "swerve/modules/" + configuration.name + "/Absolute Encoder Read Issue") + .publish(); + rawAnglePublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Angle Encoder") + .publish(); + rawDriveEncoderPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Drive Encoder") + .publish(); + rawDriveVelocityPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Drive Velocity") + .publish(); + speedSetpointPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Speed Setpoint") + .publish(); + angleSetpointPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/modules/" + configuration.name + "/Angle Setpoint") + .publish(); + } + + @Override + public void close() { + angleMotor.close(); + driveMotor.close(); + absoluteEncoder.close(); + } + + /** + * Get the default {@link SimpleMotorFeedforward} for the swerve module drive motor. + * + * @return {@link SimpleMotorFeedforward} using motor details. + */ + public SimpleMotorFeedforward getDefaultFeedforward() { + double nominalVoltage = driveMotor.getSimMotor().nominalVoltageVolts; + double maxDriveSpeedMPS = getMaxVelocity().in(MetersPerSecond); + return SwerveMath.createDriveFeedforward( + nominalVoltage, + maxDriveSpeedMPS, + configuration.physicalCharacteristics.wheelGripCoefficientOfFriction); + } + + /** + * Set utilization of {@link SwerveModuleState#optimize(Rotation2d)} which should be disabled for + * some debugging. + * + * @param optimizationState Optimization enabled. + */ + public void setModuleStateOptimization(boolean optimizationState) { + optimizeSwerveModuleState = optimizationState; + if (!optimizeSwerveModuleState) { + angleMotor.disablePIDWrapping(); + angleMotor.burnFlash(); + } + } + + /** + * Check if the module state optimization used by {@link SwerveModuleState#optimize(Rotation2d)} + * is enabled. + * + * @return optimization state. + */ + public boolean getModuleStateOptimization() { + return optimizeSwerveModuleState; + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param optimalVoltage Nominal voltage for operation to output to. + */ + public void setAngleMotorVoltageCompensation(double optimalVoltage) { + angleMotor.setVoltageCompensation(optimalVoltage); + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param optimalVoltage Nominal voltage for operation to output to. + */ + public void setDriveMotorVoltageCompensation(double optimalVoltage) { + driveMotor.setVoltageCompensation(optimalVoltage); + } + + /** Queue synchronization of the integrated angle encoder with the absolute encoder. */ + public void queueSynchronizeEncoders() { + if (absoluteEncoder != null && synchronizeEncoderEnabled) { + synchronizeEncoderQueued = true; + } + } + + /** + * Enable auto synchronization for encoders during a match. This will only occur when the modules + * are not moving for a few seconds. + * + * @param enabled Enable state + * @param deadband Deadband in degrees, default is 3 degrees. + */ + public void setEncoderAutoSynchronize(boolean enabled, double deadband) { + synchronizeEncoderEnabled = enabled; + synchronizeEncoderDeadband = deadband; + } + + /** + * Enable auto synchronization for encoders during a match. This will only occur when the modules + * are not moving for a few seconds. + * + * @param enabled Enable state + */ + public void setEncoderAutoSynchronize(boolean enabled) { + synchronizeEncoderEnabled = enabled; + } + + /** + * Set the antiJitter functionality, if true the modules will NOT auto center. Pushes the offsets + * to the angle motor controllers as well. + * + * @param antiJitter Anti-Jitter state desired. + */ + public void setAntiJitter(boolean antiJitter) { + this.antiJitterEnabled = antiJitter; + if (antiJitter) { + pushOffsetsToEncoders(); + } else { + restoreInternalOffset(); + } + } + + /** + * Set the feedforward attributes to the given parameters. + * + * @param drive Drive motor feedforward for the module. + */ + public void setFeedforward(SimpleMotorFeedforward drive) { + this.driveMotorFeedforward = drive; + } + + /** + * Get the current drive motor PIDF values. + * + * @return {@link PIDFConfig} of the drive motor. + */ + public PIDFConfig getDrivePIDF() { + return configuration.velocityPIDF; + } + + /** + * Set the drive PIDF values. + * + * @param config {@link PIDFConfig} of that should be set. + */ + public void setDrivePIDF(PIDFConfig config) { + configuration.velocityPIDF = config; + driveMotor.configurePIDF(config); + } + + /** + * Get the current angle/azimuth/steering motor PIDF values. + * + * @return {@link PIDFConfig} of the angle motor. + */ + public PIDFConfig getAnglePIDF() { + return configuration.anglePIDF; + } + + /** + * Set the angle/azimuth/steering motor PID + * + * @param config {@link PIDFConfig} of that should be set. + */ + public void setAnglePIDF(PIDFConfig config) { + configuration.anglePIDF = config; + angleMotor.configurePIDF(config); + } + + /** + * Set the desired state of the swerve module.
+ * WARNING: If you are not using one of the functions from {@link SwerveDrive} you may screw up + * {@link SwerveDrive#kinematics} + * + * @param desiredState Desired swerve module state. + * @param isOpenLoop Whether to use open loop (direct percent) or direct velocity control. + * @param force Disables optimizations that prevent movement in the angle motor and forces the + * desired state onto the swerve module. + */ + public void setDesiredState(SwerveModuleState desiredState, boolean isOpenLoop, boolean force) { + applyStateOptimizations(desiredState); + applyAntiJitter(desiredState, force); + + // Cosine compensation. + double nextVelocityMetersPerSecond = + configuration.useCosineCompensator + ? getCosineCompensatedVelocity(desiredState) + : desiredState.speedMetersPerSecond; + double curVelocityMetersPerSecond = lastState.speedMetersPerSecond; + desiredState.speedMetersPerSecond = nextVelocityMetersPerSecond; + + setDesiredState( + desiredState, + isOpenLoop, + driveMotorFeedforward.calculateWithVelocities( + curVelocityMetersPerSecond, nextVelocityMetersPerSecond)); + } + + /** + * Set the desired state of the swerve module.
+ * WARNING: If you are not using one of the functions from {@link SwerveDrive} you may screw up + * {@link SwerveDrive#kinematics} + * + * @param desiredState Desired swerve module state. + * @param isOpenLoop Whether to use open loop (direct percent) or direct velocity control. + * @param driveFeedforwardVoltage Drive motor controller feedforward as a voltage. + */ + public void setDesiredState( + SwerveModuleState desiredState, boolean isOpenLoop, double driveFeedforwardVoltage) { + if (isOpenLoop) { + double percentOutput = + desiredState.speedMetersPerSecond / maxDriveVelocity.in(MetersPerSecond); + driveMotor.setVoltage(percentOutput * 12); + } else { + driveMotor.setReference(desiredState.speedMetersPerSecond, driveFeedforwardVoltage); + } + + // Prevent module rotation if angle is the same as the previous angle. + // Synchronize encoders if queued and send in the current position as the value from the + // absolute encoder. + if (absoluteEncoder != null && synchronizeEncoderQueued && synchronizeEncoderEnabled) { + double absoluteEncoderPosition = getAbsolutePosition(); + if (Math.abs(angleMotor.getPosition() - absoluteEncoderPosition) + >= synchronizeEncoderDeadband) { + angleMotor.setPosition(absoluteEncoderPosition); + } + angleMotor.setReference(desiredState.angle.getDegrees(), 0, absoluteEncoderPosition); + synchronizeEncoderQueued = false; + } else { + angleMotor.setReference(desiredState.angle.getDegrees(), 0); + } + + lastState = desiredState; + + if (SwerveDriveTelemetry.isSimulation) { + simModule.updateStateAndPosition(desiredState); + } + + // TODO: Change and move to SwerveDriveTelemetry + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { + SwerveDriveTelemetry.desiredStatesObj[moduleNumber] = desiredState; + } + + if (SwerveDriveTelemetry.verbosity == TelemetryVerbosity.HIGH) { + speedSetpointPublisher.set(desiredState.speedMetersPerSecond); + angleSetpointPublisher.set(desiredState.angle.getDegrees()); + } + + if (moduleNumber == SwerveDriveTelemetry.moduleCount - 1) { + SwerveDriveTelemetry.endCtrlCycle(); + } + } + + /** + * Get the cosine compensated velocity to set the swerve module to. + * + * @param desiredState Desired {@link SwerveModuleState} to use. + * @return Cosine compensated velocity in meters/second. + */ + private double getCosineCompensatedVelocity(SwerveModuleState desiredState) { + double cosineScalar = 1.0; + // Taken from the CTRE SwerveModule class. + // https://api.ctr-electronics.com/phoenix6/release/java/src-html/com/ctre/phoenix6/mechanisms/swerve/SwerveModule.html#line.46 + /* From FRC 900's whitepaper, we add a cosine compensator to the applied drive velocity */ + /* To reduce the "skew" that occurs when changing direction */ + /* If error is close to 0 rotations, we're already there, so apply full power */ + /* If the error is close to 0.25 rotations, then we're 90 degrees, so movement doesn't help us at all */ + cosineScalar = + Rotation2d.fromDegrees(desiredState.angle.getDegrees()) + .minus(Rotation2d.fromDegrees(getAbsolutePosition())) + .getCos(); // TODO: Investigate angle modulus by 180. + /* Make sure we don't invert our drive, even though we shouldn't ever target over 90 degrees anyway */ + if (cosineScalar < 0.0) { + cosineScalar = 1; + } + + return desiredState.speedMetersPerSecond * cosineScalar; + } + + /** + * Apply the {@link SwerveModuleState#optimize(Rotation2d)} function if the module state + * optimization is enabled while debugging. + * + * @param desiredState The desired state to apply the optimization to. + */ + public void applyStateOptimizations(SwerveModuleState desiredState) { + // SwerveModuleState optimization might be desired to be disabled while debugging. + if (optimizeSwerveModuleState) { + desiredState.optimize(Rotation2d.fromDegrees(getAbsolutePosition())); + } + } + + /** + * Apply anti-jitter to the desired state. This will prevent the module from rotating if the speed + * requested is too low. If force is true, the anti-jitter will not be applied. + * + * @param desiredState The desired state to apply the anti-jitter to. + * @param force Whether to ignore the {@link SwerveModule#antiJitterEnabled} state and apply the + * anti-jitter anyway. + */ + public void applyAntiJitter(SwerveModuleState desiredState, boolean force) { + if (!force && antiJitterEnabled) { + // Prevents module rotation if speed is less than 1% + SwerveMath.antiJitter(desiredState, lastState, Math.min(maxDriveVelocityMetersPerSecond, 4)); + } + } + + /** + * Set the angle for the module. + * + * @param angle Angle in degrees. + */ + public void setAngle(double angle) { + angleMotor.setReference(angle, 0); + lastState.angle = Rotation2d.fromDegrees(angle); + } + + /** + * Get the Swerve Module state. + * + * @return Current SwerveModule state. + */ + public SwerveModuleState getState() { + double velocity; + Rotation2d azimuth; + if (!SwerveDriveTelemetry.isSimulation) { + velocity = driveVelocityCache.getValue(); + azimuth = Rotation2d.fromDegrees(getAbsolutePosition()); + } else { + return simModule.getState(); + } + return new SwerveModuleState(velocity, azimuth); + } + + /** + * Get the position of the swerve module. + * + * @return {@link SwerveModulePosition} of the swerve module. + */ + public SwerveModulePosition getPosition() { + double position; + Rotation2d azimuth; + if (!SwerveDriveTelemetry.isSimulation) { + position = drivePositionCache.getValue(); + azimuth = Rotation2d.fromDegrees(getAbsolutePosition()); + } else { + return simModule.getPosition(); + } + return new SwerveModulePosition(position, azimuth); + } + + /** + * Get the absolute position. Falls back to relative position on reading failure. + * + * @return Absolute encoder angle in degrees in the range [0, 360). + */ + public double getAbsolutePosition() { + return absolutePositionCache.getValue(); + } + + /** + * Get the absolute position. Falls back to relative position on reading failure. + * + * @return Absolute encoder angle in degrees in the range [0, 360). + */ + public double getRawAbsolutePosition() { + /* During simulation, when no absolute encoders are available, we return the state from the simulation module instead. */ + if (SwerveDriveTelemetry.isSimulation) { + Rotation2d absolutePosition = simModule.getState().angle; + return absolutePosition.getDegrees(); + } + + double angle; + if (absoluteEncoder != null) { + angle = absoluteEncoder.getAbsolutePosition() - angleOffset; + if (absoluteEncoder.readingError) { + angle = getRelativePosition(); + } + } else { + angle = getRelativePosition(); + } + if (optimizeSwerveModuleState) { + angle %= 360; + if (angle < 0.0) { + angle += 360; + } + } + + return angle; + } + + /** + * Get the relative angle in degrees. + * + * @return Angle in degrees. + */ + public double getRelativePosition() { + return angleMotor.getPosition(); + } + + /** + * Set the brake mode. + * + * @param brake Set the brake mode. + */ + public void setMotorBrake(boolean brake) { + driveMotor.setMotorBrake(brake); + } + + /** + * Set the conversion factor for the angle/azimuth motor controller. + * + * @param conversionFactor Angle motor conversion factor for PID, should be generated from {@link + * SwerveMath#calculateDegreesPerSteeringRotation(double, double)} or calculated. + */ + public void setAngleMotorConversionFactor(double conversionFactor) { + angleMotor.configureIntegratedEncoder(conversionFactor); + } + + /** + * Set the conversion factor for the drive motor controller. + * + * @param conversionFactor Drive motor conversion factor for PID, should be generated from {@link + * SwerveMath#calculateMetersPerRotation(double, double, double)} or calculated. + */ + public void setDriveMotorConversionFactor(double conversionFactor) { + driveMotor.configureIntegratedEncoder(conversionFactor); + } + + /** + * Get the angle {@link SwerveMotor} for the {@link SwerveModule}. + * + * @return {@link SwerveMotor} for the angle/steering motor of the module. + */ + public SwerveMotor getAngleMotor() { + return angleMotor; + } + + /** + * Get the drive {@link SwerveMotor} for the {@link SwerveModule}. + * + * @return {@link SwerveMotor} for the drive motor of the module. + */ + public SwerveMotor getDriveMotor() { + return driveMotor; + } + + /** + * Get the {@link SwerveAbsoluteEncoder} for the {@link SwerveModule}. + * + * @return {@link SwerveAbsoluteEncoder} for the swerve module. + */ + public SwerveAbsoluteEncoder getAbsoluteEncoder() { + return absoluteEncoder; + } + + /** + * Fetch the {@link SwerveModuleConfiguration} for the {@link SwerveModule} with the parsed + * configurations. + * + * @return {@link SwerveModuleConfiguration} for the {@link SwerveModule}. + */ + public SwerveModuleConfiguration getConfiguration() { + return configuration; + } + + /** Use external sensors for the feedback of the angle/azimuth/steer controller. */ + public void useExternalFeedbackSensor() { + if (absoluteEncoder == null) { + externalSensorIsNull.set(true); + return; + } + if (angleOffset == 0) { + internalOffsetIsZero.set(true); + } + if (absoluteEncoder.setAbsoluteEncoderOffset(configuration.angleOffset)) { + angleMotor.setAbsoluteEncoder(absoluteEncoder); + if (angleMotor.usingExternalFeedbackSensor()) { + angleOffset = 0; + } else { + externalFeedbackIncompatible.set(true); + angleMotor.setAbsoluteEncoder(null); + absoluteEncoder.setAbsoluteEncoderOffset(0); + } + + } else { + externalOffsetIncompatible.set(true); + absoluteEncoder.setAbsoluteEncoderOffset(0); + } + } + + /** Use external sensors for the feedback of the angle/azimuth/steer controller. */ + public void useInternalFeedbackSensor() { + if (absoluteEncoder == null) { + externalSensorIsNull.set(true); + return; + } + if (angleOffset == 0) { + internalOffsetIsZero.set(true); + } + angleMotor.setAbsoluteEncoder(null); + absoluteEncoder.setAbsoluteEncoderOffset(0); + angleOffset = configuration.angleOffset; + } + + /** + * Push absolute encoder offset in the memory of the encoder or controller. Also removes the + * internal angle offset. + */ + @Deprecated + public void pushOffsetsToEncoders() { + if (absoluteEncoder != null && angleOffset == configuration.angleOffset) { + // If the absolute encoder is attached. + if (angleMotor instanceof SparkMaxSwerve + || angleMotor instanceof SparkMaxBrushedMotorSwerve) { + if (absoluteEncoder instanceof SparkMaxEncoderSwerve) { + angleMotor.setAbsoluteEncoder(absoluteEncoder); + if (absoluteEncoder.setAbsoluteEncoderOffset(angleOffset)) { + angleOffset = 0; + } else { + angleMotor.setAbsoluteEncoder(null); + encoderOffsetWarning.set(true); + } + } + } + + } else { + noEncoderWarning.set(true); + } + } + + /** + * Restore internal offset in YAGSL and either sets absolute encoder offset to 0 or restores old + * value. + */ + public void restoreInternalOffset() { + angleMotor.setAbsoluteEncoder(null); + absoluteEncoder.setAbsoluteEncoderOffset(0); + angleOffset = configuration.angleOffset; + } + + /** + * Get if the last Absolute Encoder had a read issue, such as it does not exist. + * + * @return If the last Absolute Encoder had a read issue, or absolute encoder does not exist. + */ + public boolean getAbsoluteEncoderReadIssue() { + if (absoluteEncoder == null) { + return true; + } else { + return absoluteEncoder.readingError; + } + } + + /** + * Get the maximum module velocity as a {@link LinearVelocity} based on the RPM and gear ratio. + * + * @return {@link LinearVelocity} max velocity of the drive wheel. + */ + public LinearVelocity getMaxVelocity() { + getMaxDriveVelocityMetersPerSecond(); + return maxDriveVelocity; + } + + /** + * Get the maximum drive velocity of the module in Meters Per Second. + * + * @return Maximum drive motor velocity in Meters Per Second. + */ + public double getMaxDriveVelocityMetersPerSecond() { + if (maxDriveVelocity == null) { + maxDriveVelocity = + InchesPerSecond.of( + (driveMotor.getSimMotor().freeSpeedRadPerSec + / configuration.conversionFactors.drive.gearRatio) + * configuration.conversionFactors.drive.diameter + / 2.0); + maxDriveVelocityMetersPerSecond = maxDriveVelocity.in(MetersPerSecond); + } + return maxDriveVelocityMetersPerSecond; + } + + /** + * Get the maximum module angular velocity as a {@link AngularVelocity} based on the RPM and gear + * ratio. + * + * @return {@link AngularVelocity} max velocity of the angle/azimuth. + */ + public AngularVelocity getMaxAngularVelocity() { + if (maxAngularVelocity == null) { + maxAngularVelocity = + RotationsPerSecond.of( + RadiansPerSecond.of(angleMotor.getSimMotor().freeSpeedRadPerSec) + .in(RotationsPerSecond) + / configuration.conversionFactors.angle.gearRatio); + } + return maxAngularVelocity; + } + + /** Update data sent to {@link SmartDashboard}. */ + public void updateTelemetry() { + if (absoluteEncoder != null) { + rawAbsoluteAnglePublisher.set(absoluteEncoder.getAbsolutePosition()); + } + if (SwerveDriveTelemetry.isSimulation + && SwerveDriveTelemetry.verbosity == TelemetryVerbosity.HIGH) { + SwerveModulePosition pos = simModule.getPosition(); + SwerveModuleState state = simModule.getState(); + rawAnglePublisher.set(pos.angle.getDegrees()); + rawDriveEncoderPublisher.set(pos.distanceMeters); + rawDriveVelocityPublisher.set(state.speedMetersPerSecond); + // For code coverage + angleMotor.getPosition(); + drivePositionCache.getValue(); + driveVelocityCache.getValue(); + } else { + rawAnglePublisher.set(angleMotor.getPosition()); + rawDriveEncoderPublisher.set(drivePositionCache.getValue()); + rawDriveVelocityPublisher.set(driveVelocityCache.getValue()); + } + adjAbsoluteAnglePublisher.set(getAbsolutePosition()); + absoluteEncoderIssuePublisher.set(getAbsoluteEncoderReadIssue()); + } + + /** Invalidate the {@link Cache} objects used by {@link SwerveModule}. */ + public void invalidateCache() { + absolutePositionCache.update(); + drivePositionCache.update(); + driveVelocityCache.update(); + } + + /** + * Obtains the {@link SwerveModuleSimulation} used in simulation. + * + * @return the module simulation, null if this method is called on a real robot + */ + public SwerveModuleSimulation getSimModule() { + return simModule; + } + + /** + * Configure the {@link SwerveModule#simModule} with the MapleSim {@link + * org.ironmaple.simulation.drivesims.SwerveModuleSimulation} + * + * @param swerveModuleSimulation MapleSim {@link + * org.ironmaple.simulation.drivesims.SwerveModuleSimulation} to configure with. + * @param physicalCharacteristics {@link SwerveModulePhysicalCharacteristics} that represent the + * swerve drive. + */ + public void configureModuleSimulation( + org.ironmaple.simulation.drivesims.SwerveModuleSimulation swerveModuleSimulation, + SwerveModulePhysicalCharacteristics physicalCharacteristics) { + this.simModule.configureSimModule(swerveModuleSimulation, physicalCharacteristics); + } +} diff --git a/src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java b/src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java new file mode 100644 index 00000000..af58444c --- /dev/null +++ b/src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java @@ -0,0 +1,126 @@ +package swervelib.encoders; + +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.AnalogInput; +import edu.wpi.first.wpilibj.RobotController; + +/** Swerve Absolute Encoder for Thrifty Encoders and other analog encoders. */ +public class AnalogAbsoluteEncoderSwerve extends SwerveAbsoluteEncoder { + // Entire class inspired by 5010 + // Source: + // https://github.com/FRC5010/FRCLibrary/blob/main/FRC5010Example2023/src/main/java/frc/robot/FRC5010/sensors/AnalogInput5010.java + /** Encoder as Analog Input. */ + public AnalogInput encoder; + /** Inversion state of the encoder. */ + private boolean inverted = false; + /** An {@link Alert} for if the absolute encoder offset cannot be set. */ + private Alert cannotSetOffset; + /** + * An {@link Alert} detailing how the analog absolute encoder may not report accurate velocities. + */ + private Alert inaccurateVelocities; + + /** + * Construct the Thrifty Encoder as a Swerve Absolute Encoder. + * + * @param encoder Encoder to construct. + */ + public AnalogAbsoluteEncoderSwerve(AnalogInput encoder) { + this.encoder = encoder; + cannotSetOffset = + new Alert( + "Encoders", + "Cannot Set Absolute Encoder Offset of Analog Encoders Channel #" + + encoder.getChannel(), + AlertType.kWarning); + inaccurateVelocities = + new Alert( + "Encoders", + "The Analog Absolute encoder may not report accurate velocities!", + AlertType.kWarning); + } + + @Override + public void close() { + encoder.close(); + } + + /** + * Construct the Encoder given the analog input channel. + * + * @param channel Analog Input channel of which the encoder resides. + */ + public AnalogAbsoluteEncoderSwerve(int channel) { + this(new AnalogInput(channel)); + } + + /** Reset the encoder to factory defaults. */ + @Override + public void factoryDefault() { + // Do nothing + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + // Do nothing + } + + /** + * Configure the absolute encoder to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + this.inverted = inverted; + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + return (inverted ? -1.0 : 1.0) + * (encoder.getAverageVoltage() / RobotController.getVoltage5V()) + * 360; + } + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Cannot Set the offset of an Analog Absolute Encoder. + * + * @param offset the offset the Absolute Encoder uses as the zero point. + * @return Will always be false as setting the offset is unsupported of an Analog absolute + * encoder. + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + // Do Nothing + cannotSetOffset.set(true); + return false; + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + inaccurateVelocities.set(true); + return encoder.getValue() * 360; + } +} diff --git a/src/main/java/swervelib/encoders/CANCoderSwerve.java b/src/main/java/swervelib/encoders/CANCoderSwerve.java new file mode 100644 index 00000000..906c0c7f --- /dev/null +++ b/src/main/java/swervelib/encoders/CANCoderSwerve.java @@ -0,0 +1,211 @@ +package swervelib.encoders; + +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.Milliseconds; +import static edu.wpi.first.units.Units.Rotations; +import static edu.wpi.first.units.Units.Seconds; + +import com.ctre.phoenix6.StatusCode; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.CANcoderConfigurator; +import com.ctre.phoenix6.hardware.CANcoder; +import com.ctre.phoenix6.signals.MagnetHealthValue; +import com.ctre.phoenix6.signals.SensorDirectionValue; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; + +/** Swerve Absolute Encoder for CTRE CANCoders. */ +public class CANCoderSwerve extends SwerveAbsoluteEncoder { + + /** Wait time for status frames to show up. */ + public static double STATUS_TIMEOUT_SECONDS = Milliseconds.of(1).in(Seconds); + /** An {@link Alert} for if the CANCoder magnet field is less than ideal. */ + private final Alert magnetFieldLessThanIdeal; + /** An {@link Alert} for if the CANCoder reading is faulty. */ + private final Alert readingFaulty; + /** An {@link Alert} for if the CANCoder reading is faulty and the reading is ignored. */ + private final Alert readingIgnored; + /** An {@link Alert} for if the absolute encoder offset cannot be set. */ + private final Alert cannotSetOffset; + /** Magnet Health status signal for the CANCoder. */ + private final StatusSignal magnetHealth; + /** CANCoder reading cache. */ + private final StatusSignal angle; + /** Angular velocity of the {@link CANcoder}. */ + private final StatusSignal velocity; + /** CANCoder with WPILib sendable and support. */ + public CANcoder encoder; + /** {@link CANcoder} Configurator objet for this class. */ + private CANcoderConfigurator config; + /** {@link CANcoderConfiguration} object for the CANcoder. */ + private CANcoderConfiguration cfg = new CANcoderConfiguration(); + + /** + * Initialize the CANCoder on the standard CANBus. + * + * @param id CAN ID. + */ + public CANCoderSwerve(int id) { + // Empty string uses the default canbus for the system + this(id, ""); + } + + /** + * Initialize the CANCoder on the CANivore. + * + * @param id CAN ID of the {@link CANcoder}. + * @param canbus CAN bus to initialize it on. Should be "rio" or "" if the RIO CANbus, else is the + * CANivore name. + */ + public CANCoderSwerve(int id, String canbus) { + encoder = new CANcoder(id, canbus); + config = encoder.getConfigurator(); + magnetHealth = encoder.getMagnetHealth(); + angle = encoder.getAbsolutePosition(); + velocity = encoder.getVelocity(); + magnetFieldLessThanIdeal = + new Alert( + "Encoders", + "CANCoder " + encoder.getDeviceID() + " magnetic field is less than ideal.", + AlertType.kWarning); + readingFaulty = + new Alert( + "Encoders", + "CANCoder " + encoder.getDeviceID() + " reading was faulty.", + AlertType.kWarning); + readingIgnored = + new Alert( + "Encoders", + "CANCoder " + encoder.getDeviceID() + " reading was faulty, ignoring.", + AlertType.kWarning); + cannotSetOffset = + new Alert( + "Encoders", + "Failure to set CANCoder " + encoder.getDeviceID() + " Absolute Encoder Offset", + AlertType.kWarning); + } + + @Override + public void close() { + encoder.close(); + } + + /** Reset the encoder to factory defaults. */ + @Override + public void factoryDefault() { + cfg = new CANcoderConfiguration(); + config.apply(cfg); + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + encoder.clearStickyFaults(); + } + + /** + * Configure the absolute encoder to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + config.refresh(cfg.MagnetSensor); + config.apply( + cfg.MagnetSensor.withAbsoluteSensorDiscontinuityPoint(Rotations.of(1)) + .withSensorDirection( + inverted + ? SensorDirectionValue.Clockwise_Positive + : SensorDirectionValue.CounterClockwise_Positive)); + } + + /** + * Get the absolute position of the encoder. Sets {@link SwerveAbsoluteEncoder#readingError} on + * erroneous readings. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + readingError = false; + MagnetHealthValue strength = magnetHealth.refresh().getValue(); + angle.refresh(); + + magnetFieldLessThanIdeal.set(strength != MagnetHealthValue.Magnet_Green); + if (strength == MagnetHealthValue.Magnet_Invalid || strength == MagnetHealthValue.Magnet_Red) { + readingError = true; + readingFaulty.set(true); + return 0; + } else { + readingFaulty.set(false); + } + + // Taken from democat's library. + // Source: + // https://github.com/democat3457/swerve-lib/blob/7c03126b8c22f23a501b2c2742f9d173a5bcbc40/src/main/java/com/swervedrivespecialties/swervelib/ctre/CanCoderFactoryBuilder.java#L51-L74 + for (int i = 0; i < maximumRetries; i++) { + if (angle.getStatus() == StatusCode.OK) { + break; + } + angle.waitForUpdate(STATUS_TIMEOUT_SECONDS); + } + if (angle.getStatus() != StatusCode.OK) { + readingError = true; + readingIgnored.set(true); + } else { + readingIgnored.set(false); + } + // Convert from Rotations to Degrees. + return angle.getValueAsDouble() * 360; + } + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Sets the Absolute Encoder Offset within the CANcoder's Memory. + * + * @param offset the offset the Absolute Encoder uses as the zero point in degrees. + * @return if setting Absolute Encoder Offset was successful or not. + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + StatusCode error = config.refresh(cfg.MagnetSensor); + if (error != StatusCode.OK) { + return false; + } + + error = config.apply(cfg.MagnetSensor.withMagnetOffset(offset / 360)); + cannotSetOffset.setText( + "Failure to set CANCoder " + + encoder.getDeviceID() + + " Absolute Encoder Offset Error: " + + error); + if (error == StatusCode.OK) { + cannotSetOffset.set(false); + return true; + } + cannotSetOffset.set(true); + return false; + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + return velocity.refresh().getValue().in(DegreesPerSecond); + } +} diff --git a/src/main/java/swervelib/encoders/CanAndMagSwerve.java b/src/main/java/swervelib/encoders/CanAndMagSwerve.java new file mode 100644 index 00000000..f8bff1d9 --- /dev/null +++ b/src/main/java/swervelib/encoders/CanAndMagSwerve.java @@ -0,0 +1,97 @@ +package swervelib.encoders; + +import com.reduxrobotics.sensors.canandmag.Canandmag; +import com.reduxrobotics.sensors.canandmag.CanandmagSettings; + +/** HELIUM {@link Canandmag} from ReduxRobotics absolute encoder, attached through the CAN bus. */ +public class CanAndMagSwerve extends SwerveAbsoluteEncoder { + + /** The {@link Canandmag} representing the CANandMag on the CAN bus. */ + public Canandmag encoder; + /** The {@link Canandmag} settings object to use. */ + public CanandmagSettings settings; + + /** + * Create the {@link Canandmag} + * + * @param canid The CAN ID whenever the CANandMag is operating on the CANBus. + */ + public CanAndMagSwerve(int canid) { + encoder = new Canandmag(canid); + settings = new CanandmagSettings(); + } + + @Override + public void close() { + encoder.close(); + } + + /** + * Reset the encoder to factory defaults. + * + *

This will not clear the stored zero offset. + */ + @Override + public void factoryDefault() { + encoder.resetFactoryDefaults(false); + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + encoder.clearStickyFaults(); + } + + /** + * Configure the CANandMag to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + settings.setInvertDirection(inverted); + encoder.setSettings(settings); + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + return encoder.getAbsPosition() * 360; + } + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Cannot set the offset of the CANandMag. + * + * @param offset the offset the Absolute Encoder uses as the zero point. + * @return true if setting the zero point succeeded, false otherwise + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + settings.setZeroOffset(offset); + return encoder.setSettings(settings); + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + return encoder.getVelocity() * 360; + } +} diff --git a/src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java b/src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java new file mode 100644 index 00000000..112ba3a8 --- /dev/null +++ b/src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java @@ -0,0 +1,106 @@ +package swervelib.encoders; + +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.DutyCycleEncoder; +import edu.wpi.first.wpilibj.Timer; + +/** + * DutyCycle encoders such as "US Digital MA3 with DIO Output, the CTRE Mag Encoder, the Rev Hex + * Encoder, and the AM Mag Encoder." attached via a DIO lane. + * + *

Credits to + * p2reneker25 for building this. + */ +public class DIODutyCycleEncoderSwerve extends SwerveAbsoluteEncoder { + + /** Duty Cycle Encoder. */ + private final DutyCycleEncoder encoder; + /** Inversion state. */ + private boolean isInverted; + /** An {@link Alert} for if the encoder cannot report accurate velocities. */ + private Alert inaccurateVelocities; + /** The Offset in degrees of the DIO absolute encoder. */ + private double offset; + + /** + * Constructor for the DIO duty cycle encoder. + * + * @param pin DIO lane for the encoder. + */ + public DIODutyCycleEncoderSwerve(int pin) { + encoder = new DutyCycleEncoder(pin); + Timer.delay(2); + inaccurateVelocities = + new Alert( + "Encoders", + "The DIO Duty Cycle encoder may not report accurate velocities!", + AlertType.kWarning); + } + + @Override + public void close() { + encoder.close(); + } + + /** + * Configure the inversion state of the encoder. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + isInverted = inverted; + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + return (isInverted ? -1.0 : 1.0) * ((encoder.get() * 360) - offset); + } + + /** + * Get the encoder object. + * + * @return {@link DutyCycleEncoder} from the class. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + inaccurateVelocities.set(true); + return encoder.get(); + } + + /** Reset the encoder to factory defaults. */ + @Override + public void factoryDefault() { + // Do nothing + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + // Do nothing + } + + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + this.offset = offset; + + return true; + } +} diff --git a/src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java b/src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java new file mode 100644 index 00000000..e36159cc --- /dev/null +++ b/src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java @@ -0,0 +1,136 @@ +package swervelib.encoders; + +import com.revrobotics.AbsoluteEncoder; +import com.revrobotics.spark.SparkAbsoluteEncoder; +import com.revrobotics.spark.SparkFlex; +import com.revrobotics.spark.config.SparkFlexConfig; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import swervelib.motors.SparkFlexSwerve; +import swervelib.motors.SwerveMotor; + +/** SparkFlex absolute encoder, attached through the data port. */ +public class SparkFlexEncoderSwerve extends SwerveAbsoluteEncoder { + + /** The {@link AbsoluteEncoder} representing the duty cycle encoder attached to the SparkFlex. */ + public SparkAbsoluteEncoder encoder; + /** An {@link Alert} for if there is a failure configuring the encoder. */ + private Alert failureConfiguring; + /** {@link SparkFlexSwerve} instance. */ + private SwerveMotor sparkFlex; + + /** + * Create the {@link SparkFlexEncoderSwerve} object as a duty cycle from the {@link SparkFlex} + * motor. + * + * @param motor Motor to create the encoder from. + * @param conversionFactor The conversion factor to set if the output is not from 0 to 360. + */ + public SparkFlexEncoderSwerve(SwerveMotor motor, int conversionFactor) { + failureConfiguring = + new Alert("Encoders", "Failure configuring SparkFlex Absolute Encoder", AlertType.kWarning); + if (motor.getMotor() instanceof SparkFlex) { + sparkFlex = motor; + encoder = ((SparkFlex) motor.getMotor()).getAbsoluteEncoder(); + setConversionFactor(conversionFactor); + } else { + throw new RuntimeException( + "Motor given to instantiate SparkFlexEncoder is not a CANSparkFlex"); + } + } + + @Override + public void close() { + // SPARK Flex encoder gets closed with the motor + // I don't think an encoder getting closed should + // close the entire motor so i will keep this empty + // sparkFlex.close(); + } + + /** Reset the encoder to factory defaults. */ + @Override + public void factoryDefault() { + // Do nothing + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + // Do nothing + } + + /** + * Configure the absolute encoder to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + if (sparkFlex instanceof SparkFlexSwerve) { + SparkFlexConfig cfg = ((SparkFlexSwerve) sparkFlex).getConfig(); + cfg.absoluteEncoder.inverted(inverted); + ((SparkFlexSwerve) sparkFlex).updateConfig(cfg); + } + } + + /** + * Set the conversion factor of the {@link SparkFlexEncoderSwerve}. + * + * @param conversionFactor Position conversion factor from ticks to unit. + */ + public void setConversionFactor(double conversionFactor) { + SparkFlexConfig cfg = ((SparkFlexSwerve) sparkFlex).getConfig(); + cfg.signals.absoluteEncoderPositionAlwaysOn(true).absoluteEncoderPositionPeriodMs(20); + cfg.absoluteEncoder + .positionConversionFactor(conversionFactor) + .velocityConversionFactor(conversionFactor / 60); + ((SparkFlexSwerve) sparkFlex).updateConfig(cfg); + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + return encoder.getPosition(); + } + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Sets the Absolute Encoder Offset inside of the SparkFlex's Memory. + * + * @param offset the offset the Absolute Encoder uses as the zero point. + * @return if setting Absolute Encoder Offset was successful or not. + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + if (sparkFlex instanceof SparkFlexSwerve) { + SparkFlexConfig cfg = ((SparkFlexSwerve) sparkFlex).getConfig(); + cfg.absoluteEncoder.zeroOffset(offset); + ((SparkFlexSwerve) sparkFlex).updateConfig(cfg); + return true; + } + return false; + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + return encoder.getVelocity(); + } +} diff --git a/src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java b/src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java new file mode 100644 index 00000000..cbb4a7ad --- /dev/null +++ b/src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java @@ -0,0 +1,185 @@ +package swervelib.encoders; + +import com.revrobotics.REVLibError; +import com.revrobotics.spark.SparkAnalogSensor; +import com.revrobotics.spark.SparkMax; +import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; +import com.revrobotics.spark.config.SparkMaxConfig; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import java.util.function.Supplier; +import swervelib.motors.SparkMaxBrushedMotorSwerve; +import swervelib.motors.SparkMaxSwerve; +import swervelib.motors.SwerveMotor; + +/** SparkMax absolute encoder, attached through the data port analog pin. */ +public class SparkMaxAnalogEncoderSwerve extends SwerveAbsoluteEncoder { + + /** + * {@link swervelib.motors.SparkMaxSwerve} or {@link swervelib.motors.SparkMaxBrushedMotorSwerve} + * object. + */ + private final SwerveMotor sparkMax; + /** + * The {@link SparkAnalogSensor} representing the duty cycle encoder attached to the SparkMax + * analog port. + */ + public SparkAnalogSensor encoder; + /** An {@link Alert} for if there is a failure configuring the encoder. */ + private Alert failureConfiguring; + /** An {@link Alert} for if the absolute encoder does not support integrated offsets. */ + private Alert doesNotSupportIntegratedOffsets; + + /** + * Create the {@link SparkMaxAnalogEncoderSwerve} object as a analog sensor from the {@link + * SparkMax} motor data port analog pin. + * + * @param motor Motor to create the encoder from. + * @param maxVoltage Maximum voltage for analog input reading. + */ + public SparkMaxAnalogEncoderSwerve(SwerveMotor motor, double maxVoltage) { + if (motor.getMotor() instanceof SparkMax) { + sparkMax = motor; + encoder = ((SparkMax) motor.getMotor()).getAnalog(); + setConversionFactor(360.0 / maxVoltage); + } else { + throw new RuntimeException("Motor given to instantiate SparkMaxEncoder is not a CANSparkMax"); + } + failureConfiguring = + new Alert("Encoders", "Failure configuring SparkMax Analog Encoder", AlertType.kWarning); + doesNotSupportIntegratedOffsets = + new Alert( + "Encoders", + "SparkMax Analog Sensors do not support integrated offsets", + AlertType.kWarning); + } + + @Override + public void close() { + // SPARK MAX Analog encoder gets closed with the motor + // I don't think an encoder getting closed should + // close the entire motor so i will keep this empty + // sparkMax.close(); + } + + /** + * Run the configuration until it succeeds or times out. + * + * @param config Lambda supplier returning the error state. + */ + private void configureSparkMax(Supplier config) { + for (int i = 0; i < maximumRetries; i++) { + if (config.get() == REVLibError.kOk) { + return; + } + } + failureConfiguring.set(true); + } + + /** + * Set the conversion factor of the {@link SparkMaxAnalogEncoderSwerve}. + * + * @param conversionFactor Position conversion factor from ticks to unit. + */ + public void setConversionFactor(double conversionFactor) { + SparkMaxConfig cfg = null; + if (sparkMax instanceof SparkMaxSwerve) { + cfg = ((SparkMaxSwerve) sparkMax).getConfig(); + + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); + } + if (cfg != null) { + cfg.closedLoop.feedbackSensor(FeedbackSensor.kAnalogSensor); + + cfg.signals + .analogVelocityAlwaysOn(true) + .analogVoltageAlwaysOn(true) + .analogPositionAlwaysOn(true) + .analogVoltagePeriodMs(20) + .analogPositionPeriodMs(20) + .analogVelocityPeriodMs(20); + + cfg.analogSensor + .positionConversionFactor(conversionFactor) + .velocityConversionFactor(conversionFactor / 60); + } + if (sparkMax instanceof SparkMaxSwerve) { + ((SparkMaxSwerve) sparkMax).updateConfig(cfg); + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); + } + } + + /** Reset the encoder to factory defaults. */ + @Override + public void factoryDefault() { + // Do nothing + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + // Do nothing + } + + /** + * Configure the absolute encoder to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + if (sparkMax instanceof SparkMaxSwerve) { + SparkMaxConfig cfg = ((SparkMaxSwerve) sparkMax).getConfig(); + cfg.analogSensor.inverted(inverted); + ((SparkMaxSwerve) sparkMax).updateConfig(cfg); + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + SparkMaxConfig cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); + cfg.analogSensor.inverted(inverted); + ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); + } + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + return encoder.getPosition(); + } + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Sets the Absolute Encoder offset at the Encoder Level. + * + * @param offset the offset the Absolute Encoder uses as the zero point. + * @return if setting Absolute Encoder Offset was successful or not. + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + doesNotSupportIntegratedOffsets.set(true); + return false; + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + return encoder.getVelocity(); + } +} diff --git a/src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java b/src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java new file mode 100644 index 00000000..cc2ad1ba --- /dev/null +++ b/src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java @@ -0,0 +1,190 @@ +package swervelib.encoders; + +import com.revrobotics.AbsoluteEncoder; +import com.revrobotics.REVLibError; +import com.revrobotics.spark.SparkAbsoluteEncoder; +import com.revrobotics.spark.SparkMax; +import com.revrobotics.spark.config.SparkMaxConfig; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import java.util.function.Supplier; +import swervelib.motors.SparkMaxBrushedMotorSwerve; +import swervelib.motors.SparkMaxSwerve; +import swervelib.motors.SwerveMotor; + +/** SparkMax absolute encoder, attached through the data port. */ +public class SparkMaxEncoderSwerve extends SwerveAbsoluteEncoder { + + /** The {@link AbsoluteEncoder} representing the duty cycle encoder attached to the SparkMax. */ + public SparkAbsoluteEncoder encoder; + /** An {@link Alert} for if there is a failure configuring the encoder. */ + private Alert failureConfiguring; + /** An {@link Alert} for if there is a failure configuring the encoder offset. */ + private Alert offsetFailure; + /** {@link SparkMaxBrushedMotorSwerve} or {@link SparkMaxSwerve} instance. */ + private SwerveMotor sparkMax; + + /** + * Create the {@link SparkMaxEncoderSwerve} object as a duty cycle from the {@link + * com.revrobotics.spark.SparkMax} motor. + * + * @param motor Motor to create the encoder from. + * @param conversionFactor The conversion factor to set if the output is not from 0 to 360. + */ + public SparkMaxEncoderSwerve(SwerveMotor motor, int conversionFactor) { + failureConfiguring = + new Alert("Encoders", "Failure configuring SparkMax Absolute Encoder", AlertType.kWarning); + offsetFailure = + new Alert("Encoders", "Failure to set Absolute Encoder Offset", AlertType.kWarning); + if (motor.getMotor() instanceof SparkMax) { + sparkMax = motor; + encoder = ((SparkMax) motor.getMotor()).getAbsoluteEncoder(); + setConversionFactor(conversionFactor); + } else { + throw new RuntimeException("Motor given to instantiate SparkMaxEncoder is not a CANSparkMax"); + } + } + + @Override + public void close() { + // SPARK MAX encoder gets closed with the motor + // I don't think an encoder getting closed should + // close the entire motor so i will keep this empty + // sparkFlex.close(); + } + + /** + * Run the configuration until it succeeds or times out. + * + * @param config Lambda supplier returning the error state. + */ + private void configureSparkMax(Supplier config) { + for (int i = 0; i < maximumRetries; i++) { + if (config.get() == REVLibError.kOk) { + return; + } + } + failureConfiguring.set(true); + } + + /** Reset the encoder to factory defaults. */ + @Override + public void factoryDefault() { + // Do nothing + } + + /** Clear sticky faults on the encoder. */ + @Override + public void clearStickyFaults() { + // Do nothing + } + + /** + * Configure the absolute encoder to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + if (sparkMax instanceof SparkMaxSwerve) { + SparkMaxConfig cfg = ((SparkMaxSwerve) sparkMax).getConfig(); + cfg.absoluteEncoder.inverted(inverted); + ((SparkMaxSwerve) sparkMax).updateConfig(cfg); + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + SparkMaxConfig cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); + cfg.absoluteEncoder.inverted(inverted); + ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); + } + } + + /** + * Set the conversion factor of the {@link SparkMaxEncoderSwerve}. + * + * @param conversionFactor Position conversion factor from ticks to unit. + */ + public void setConversionFactor(double conversionFactor) { + // By default the SparkMax relays the info from the duty cycle encoder to the roborio every + // 200ms on CAN frame 5 + // This needs to be set to 20ms or under to properly update the swerve module position for + // odometry + // Configuration taken from 3005, the team who helped develop the Max Swerve: + // https://github.com/FRC3005/Charged-Up-2023-Public/blob/2b6a7c695e23edebafa27a76cf639a00f6e8a3a6/src/main/java/frc/robot/subsystems/drive/REVSwerveModule.java#L227-L244 + // Some of the frames can probably be adjusted to decrease CAN utilization, with 65535 being the + // max. + // From testing, 20ms on frame 5 sometimes returns the same value while constantly powering the + // azimuth but 8ms may be overkill, + // with limited testing 19ms did not return the same value while the module was constatntly + // rotating. + + SparkMaxConfig cfg = null; + if (sparkMax instanceof SparkMaxSwerve) { + cfg = ((SparkMaxSwerve) sparkMax).getConfig(); + + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); + } + if (cfg != null) { + cfg.signals.absoluteEncoderPositionAlwaysOn(true).absoluteEncoderPositionPeriodMs(20); + + cfg.absoluteEncoder + .positionConversionFactor(conversionFactor) + .velocityConversionFactor(conversionFactor / 60); + } + if (sparkMax instanceof SparkMaxSwerve) { + ((SparkMaxSwerve) sparkMax).updateConfig(cfg); + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); + } + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + return encoder.getPosition(); + } + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + @Override + public Object getAbsoluteEncoder() { + return encoder; + } + + /** + * Sets the Absolute Encoder Offset inside of the SparkMax's Memory. + * + * @param offset the offset the Absolute Encoder uses as the zero point. + * @return if setting Absolute Encoder Offset was successful or not. + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + if (sparkMax instanceof SparkMaxSwerve) { + SparkMaxConfig cfg = ((SparkMaxSwerve) sparkMax).getConfig(); + cfg.absoluteEncoder.zeroOffset(offset); + ((SparkMaxSwerve) sparkMax).updateConfig(cfg); + return true; + } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { + SparkMaxConfig cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); + cfg.absoluteEncoder.zeroOffset(offset); + ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); + } + return false; + } + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + @Override + public double getVelocity() { + return encoder.getVelocity(); + } +} diff --git a/src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java b/src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java new file mode 100644 index 00000000..a0ff511e --- /dev/null +++ b/src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java @@ -0,0 +1,64 @@ +package swervelib.encoders; + +/** + * Swerve abstraction class to define a standard interface with absolute encoders for swerve + * modules.. + */ +public abstract class SwerveAbsoluteEncoder implements AutoCloseable { + + // This is a bit weird because some encoders are closable + // while some get closed with the motor controller + // so for some encoders this will be an empty function + @Override + public abstract void close(); + + /** + * The maximum amount of times the swerve encoder will attempt to configure itself if failures + * occur. + */ + public final int maximumRetries = 5; + /** Last angle reading was faulty. */ + public boolean readingError = false; + + /** Reset the encoder to factory defaults. */ + public abstract void factoryDefault(); + + /** Clear sticky faults on the encoder. */ + public abstract void clearStickyFaults(); + + /** + * Configure the absolute encoder to read from [0, 360) per second. + * + * @param inverted Whether the encoder is inverted. + */ + public abstract void configure(boolean inverted); + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + public abstract double getAbsolutePosition(); + + /** + * Get the instantiated absolute encoder Object. + * + * @return Absolute encoder object. + */ + public abstract Object getAbsoluteEncoder(); + + /** + * Sets the Absolute Encoder offset at the Encoder Level. + * + * @param offset the offset the Absolute Encoder uses as the zero point in degrees. + * @return if setting Absolute Encoder Offset was successful or not. + */ + public abstract boolean setAbsoluteEncoderOffset(double offset); + + /** + * Get the velocity in degrees/sec. + * + * @return velocity in degrees/sec. + */ + public abstract double getVelocity(); +} diff --git a/src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java b/src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java new file mode 100644 index 00000000..521faa79 --- /dev/null +++ b/src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java @@ -0,0 +1,82 @@ +package swervelib.encoders; + +import com.ctre.phoenix.motorcontrol.FeedbackDevice; +import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX; +import swervelib.motors.SwerveMotor; +import swervelib.motors.TalonSRXSwerve; + +/** Talon SRX attached absolute encoder. */ +public class TalonSRXEncoderSwerve extends SwerveAbsoluteEncoder { + + /** Multiplying by this converts native Talon SRX units into degrees. */ + private final double degreesPerSensorUnit; + /** Reference to a Talon SRX for polling its attached absolute encoder. */ + private final WPI_TalonSRX talon; + + /** + * Creates a {@link TalonSRXEncoderSwerve}. + * + * @param motor motor to poll the sensor from. + * @param feedbackDevice the feedback device the sensor uses e.g. PWM or Analog. + */ + public TalonSRXEncoderSwerve(SwerveMotor motor, FeedbackDevice feedbackDevice) { + if (motor instanceof TalonSRXSwerve talonSRXSwerve) { + talonSRXSwerve.setSelectedFeedbackDevice(feedbackDevice); + this.talon = (WPI_TalonSRX) talonSRXSwerve.getMotor(); + // https://v5.docs.ctr-electronics.com/en/stable/ch14_MCSensor.html#sensor-resolution + degreesPerSensorUnit = + switch (feedbackDevice) { + case Analog -> 360.0 / 1024.0; + default -> 360.0 / 4096.0; + }; + } else { + throw new RuntimeException( + "Motor given to instantiate TalonSRXEncoder is not a WPI_TalonSRX"); + } + } + + @Override + public void close() { + // TalonSRX encoder gets closed with the motor + // I don't think an encoder getting closed should + // close the entire motor so i will keep this empty + // sparkFlex.close(); + } + + @Override + public void factoryDefault() { + // Handled in TalonSRXSwerve + } + + @Override + public void clearStickyFaults() { + // Handled in TalonSRXSwerve + } + + @Override + public void configure(boolean inverted) { + talon.setSensorPhase(inverted); + } + + @Override + public double getAbsolutePosition() { + return (talon.getSelectedSensorPosition() * degreesPerSensorUnit) % 360; + } + + @Override + public Object getAbsoluteEncoder() { + return talon; + } + + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + talon.setSelectedSensorPosition( + talon.getSelectedSensorPosition() + offset / degreesPerSensorUnit); + return true; + } + + @Override + public double getVelocity() { + return talon.getSelectedSensorVelocity() * 10 * degreesPerSensorUnit; + } +} diff --git a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java new file mode 100644 index 00000000..5e1a80b6 --- /dev/null +++ b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java @@ -0,0 +1,105 @@ +package swervelib.encoders; + +import com.thethriftybot.Conversion; +import com.thethriftybot.Conversion.PositionUnit; +import com.thethriftybot.Conversion.VelocityUnit; +import com.thethriftybot.ThriftyNova; +import com.thethriftybot.ThriftyNova.EncoderType; +import com.thethriftybot.ThriftyNova.ExternalEncoder; +import swervelib.motors.SwerveMotor; +import swervelib.motors.ThriftyNovaSwerve; + +/** Thrifty Nova absolute encoder, attached through the data port. */ +public class ThriftyNovaEncoderSwerve extends SwerveAbsoluteEncoder { + + /** The absolute encoder is directly interfaced through the Thrifty Nova motor. */ + protected ThriftyNova motor; + /** Inversion state of the attached encoder. */ + protected boolean inverted = false; + /** Offset of the absolute encoder. */ + protected double offset = 0.0; + /** Position conversion object for the motor encoder */ + private Conversion positionConversion; + /** Velocity conversion object for the motor encoder */ + private Conversion velocityConversion; + + /** + * Create the {@link ThriftyNovaEncoderSwerve} object as an absolute encoder from the {@link + * ThriftyNovaSwerve} motor. + * + * @param motor {@link SwerveMotor} through which to interface with the attached encoder . + */ + public ThriftyNovaEncoderSwerve(SwerveMotor motor, String encoderType) { + this.motor = (ThriftyNova) motor.getMotor(); + positionConversion = new Conversion(PositionUnit.DEGREES, EncoderType.ABS); + velocityConversion = new Conversion(VelocityUnit.DEGREES_PER_SEC, EncoderType.ABS); + this.motor.setExternalEncoder(ExternalEncoder.valueOf(encoderType)); + this.motor.useEncoderType(EncoderType.ABS); + } + + @Override + public void close() { + // ThriftyNova encoder gets closed with the motor + // I don't think an encoder getting closed should + // close the entire motor so i will keep this empty + } + + /** Set factory default. */ + @Override + public void factoryDefault() {} + + /** Clear sticky faults. */ + @Override + public void clearStickyFaults() {} + + /** + * Configure the absolute encoder. + * + * @param inverted Whether the encoder is inverted. + */ + @Override + public void configure(boolean inverted) { + this.inverted = inverted; + } + + /** + * Get the absolute position of the encoder. + * + * @return Absolute position in degrees from [0, 360). + */ + @Override + public double getAbsolutePosition() { + double rawMotor = motor.getPosition(); + double convertedMotor = positionConversion.fromMotor(rawMotor); + return (convertedMotor + offset) * (inverted ? -1.0 : 1.0); + } + + /** Get the instantiated absolute encoder Object. */ + @Override + public Object getAbsoluteEncoder() { + return null; + } + + /** + * Set the absolute encoder offset. + * + * @param offset offset in degrees from [0, 360). + * @return true if successful. + */ + @Override + public boolean setAbsoluteEncoderOffset(double offset) { + this.offset = offset; + return true; + } + + /** + * Get the absolute encoder velocity. WARNING: Angular velocity is generally not measurable at + * high speeds. + * + * @return Velocity in degrees per second. + */ + @Override + public double getVelocity() { + return velocityConversion.fromMotor(motor.getVelocity()) * (inverted ? -1.0 : 1.0); + } +} diff --git a/src/main/java/swervelib/encoders/package-info.java b/src/main/java/swervelib/encoders/package-info.java new file mode 100644 index 00000000..6b93a2d5 --- /dev/null +++ b/src/main/java/swervelib/encoders/package-info.java @@ -0,0 +1,5 @@ +/** + * Absolute encoders for the swerve drive, all implement {@link + * swervelib.encoders.SwerveAbsoluteEncoder}. + */ +package swervelib.encoders; diff --git a/src/main/java/swervelib/imu/ADIS16448Swerve.java b/src/main/java/swervelib/imu/ADIS16448Swerve.java new file mode 100644 index 00000000..44b14048 --- /dev/null +++ b/src/main/java/swervelib/imu/ADIS16448Swerve.java @@ -0,0 +1,120 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.ADIS16448_IMU; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; + +/** IMU Swerve class for the {@link ADIS16448_IMU} device. */ +public class ADIS16448Swerve extends SwerveIMU { + + /** {@link ADIS16448_IMU} device to read the current headings from. */ + private final ADIS16448_IMU imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** Offset for the ADIS16448. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Construct the ADIS16448 imu and reset default configurations. Publish the gyro to the + * SmartDashboard. + */ + public ADIS16448Swerve() { + imu = new ADIS16448_IMU(); + factoryDefault(); + SmartDashboard.putData(imu); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset IMU to factory default. */ + @Override + public void factoryDefault() { + offset = new Rotation3d(0, 0, 0); + imu.calibrate(); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() { + // Do nothing. + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + public Rotation3d getRawRotation3d() { + Rotation3d reading = + new Rotation3d( + Math.toRadians(-imu.getGyroAngleX()), + Math.toRadians(-imu.getGyroAngleY()), + Math.toRadians(-imu.getGyroAngleZ())); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration. + */ + @Override + public Optional getAccel() { + return Optional.of(new Translation3d(imu.getAccelX(), imu.getAccelY(), imu.getAccelZ())); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/ADIS16470Swerve.java b/src/main/java/swervelib/imu/ADIS16470Swerve.java new file mode 100644 index 00000000..68328dec --- /dev/null +++ b/src/main/java/swervelib/imu/ADIS16470Swerve.java @@ -0,0 +1,121 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.ADIS16470_IMU; +import edu.wpi.first.wpilibj.ADIS16470_IMU.IMUAxis; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; + +/** IMU Swerve class for the {@link ADIS16470_IMU} device. */ +public class ADIS16470Swerve extends SwerveIMU { + + /** {@link ADIS16470_IMU} device to read the current headings from. */ + private final ADIS16470_IMU imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** Offset for the ADIS16470. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Construct the ADIS16470 imu and reset default configurations. Publish the gyro to the + * SmartDashboard. + */ + public ADIS16470Swerve() { + imu = new ADIS16470_IMU(); + offset = new Rotation3d(); + factoryDefault(); + SmartDashboard.putData(imu); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset IMU to factory default. */ + @Override + public void factoryDefault() { + offset = new Rotation3d(0, 0, 0); + imu.calibrate(); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() { + // Do nothing. + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + public Rotation3d getRawRotation3d() { + Rotation3d reading = + new Rotation3d( + Math.toRadians(imu.getAngle(IMUAxis.kRoll)), + Math.toRadians(imu.getAngle(IMUAxis.kPitch)), + Math.toRadians(imu.getAngle(IMUAxis.kYaw))); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + return Optional.of(new Translation3d(imu.getAccelX(), imu.getAccelY(), imu.getAccelZ())); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/ADXRS450Swerve.java b/src/main/java/swervelib/imu/ADXRS450Swerve.java new file mode 100644 index 00000000..965a9a87 --- /dev/null +++ b/src/main/java/swervelib/imu/ADXRS450Swerve.java @@ -0,0 +1,115 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.ADXRS450_Gyro; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; + +/** IMU Swerve class for the {@link ADXRS450_Gyro} device. */ +public class ADXRS450Swerve extends SwerveIMU { + + /** {@link ADXRS450_Gyro} device to read the current headings from. */ + private final ADXRS450_Gyro imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** Offset for the ADXRS450. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Construct the ADXRS450 imu and reset default configurations. Publish the gyro to the + * SmartDashboard. + */ + public ADXRS450Swerve() { + imu = new ADXRS450_Gyro(); + factoryDefault(); + SmartDashboard.putData(imu); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset IMU to factory default. */ + @Override + public void factoryDefault() { + imu.calibrate(); + offset = new Rotation3d(0, 0, 0); // Math.toRadians(-imu.getAngle())); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() { + // Do nothing. + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + public Rotation3d getRawRotation3d() { + Rotation3d reading = new Rotation3d(0, 0, Math.toRadians(-imu.getAngle())); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + return Optional.empty(); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/AnalogGyroSwerve.java b/src/main/java/swervelib/imu/AnalogGyroSwerve.java new file mode 100644 index 00000000..33206dc1 --- /dev/null +++ b/src/main/java/swervelib/imu/AnalogGyroSwerve.java @@ -0,0 +1,120 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.AnalogGyro; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; + +/** Creates a IMU for {@link edu.wpi.first.wpilibj.AnalogGyro} devices, only uses yaw. */ +public class AnalogGyroSwerve extends SwerveIMU { + + /** Gyroscope object. */ + private final AnalogGyro imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** Offset for the analog gyro. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Analog port in which the gyroscope is connected. Can only be attached to analog ports 0 or 1. + * + * @param channel Analog port 0 or 1. + */ + public AnalogGyroSwerve(int channel) { + if (!(channel == 0 || channel == 1)) { + throw new RuntimeException( + "Analog Gyroscope must be attached to port 0 or 1 on the roboRIO.\n"); + } + imu = new AnalogGyro(channel); + factoryDefault(); + SmartDashboard.putData(imu); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset IMU to factory default. */ + @Override + public void factoryDefault() { + imu.calibrate(); + offset = new Rotation3d(0, 0, 0); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() { + // Do nothing. + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + public Rotation3d getRawRotation3d() { + Rotation3d reading = new Rotation3d(0, 0, Math.toRadians(-imu.getAngle())); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + return Optional.empty(); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/CanandgyroSwerve.java b/src/main/java/swervelib/imu/CanandgyroSwerve.java new file mode 100644 index 00000000..0928e2ce --- /dev/null +++ b/src/main/java/swervelib/imu/CanandgyroSwerve.java @@ -0,0 +1,117 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.RotationsPerSecond; + +import com.reduxrobotics.sensors.canandgyro.Canandgyro; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import java.util.Optional; + +/** SwerveIMU interface for the Boron {@link Canandgyro} by Redux Robotics */ +public class CanandgyroSwerve extends SwerveIMU { + + /** Wait time for status frames to show up. */ + public static double STATUS_TIMEOUT_SECONDS = 0.04; + /** Boron {@link Canandgyro} by Redux Robotics. */ + private final Canandgyro imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, RotationsPerSecond); + /** Offset for the Boron {@link Canandgyro}. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Generate the SwerveIMU for {@link Canandgyro}. + * + * @param canid CAN ID for the Boron {@link Canandgyro} + */ + public CanandgyroSwerve(int canid) { + imu = new Canandgyro(canid); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset {@link Canandgyro} to factory default. */ + @Override + public void factoryDefault() { + imu.resetFactoryDefaults(STATUS_TIMEOUT_SECONDS); + } + + /** Clear sticky faults on {@link Canandgyro}. */ + @Override + public void clearStickyFaults() { + imu.clearStickyFaults(); + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRawRotation3d() { + Rotation3d reading = imu.getRotation3d(); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + + return Optional.of( + new Translation3d(imu.getAccelerationFrame().getValue()).times(9.81 / 16384.0)); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getAngularVelocityYaw()); + } + + /** + * Get the instantiated {@link Canandgyro} IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/NavXSwerve.java b/src/main/java/swervelib/imu/NavXSwerve.java new file mode 100644 index 00000000..f0c50c8e --- /dev/null +++ b/src/main/java/swervelib/imu/NavXSwerve.java @@ -0,0 +1,137 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import com.studica.frc.AHRS; +import com.studica.frc.AHRS.NavXComType; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import java.util.Optional; + +/** Communicates with the NavX({@link AHRS}) as the IMU. */ +public class NavXSwerve extends SwerveIMU { + + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** NavX IMU. */ + private AHRS imu; + /** Offset for the NavX. */ + private Rotation3d offset = new Rotation3d(); + /** An {@link Alert} for if there is an error instantiating the NavX. */ + private Alert navXError; + /** Inversion state of the {@link AHRS}. */ + private boolean inverted = false; + + /** + * Constructor for the NavX({@link AHRS}) swerve. + * + * @param port Serial Port to connect to. + */ + public NavXSwerve(NavXComType port) { + navXError = new Alert("IMU", "Error instantiating NavX.", AlertType.kError); + try { + /* Communicate w/navX-MXP via the MXP SPI Bus. */ + /* Alternatively: I2C.Port.kMXP, SerialPort.Port.kMXP or SerialPort.Port.kUSB */ + /* See http://navx-mxp.kauailabs.com/guidance/selecting-an-interface/ for details. */ + imu = new AHRS(port); + factoryDefault(); + } catch (RuntimeException ex) { + navXError.setText("Error instantiating NavX: " + ex.getMessage()); + navXError.set(true); + } + } + + @Override + public void close() { + imu.close(); + } + + /** + * Reset offset to current gyro reading. Does not call NavX({@link AHRS#reset()}) because it has + * been reported to be too slow. + */ + @Override + public void factoryDefault() { + // gyro.reset(); // Reported to be slow + offset = imu.getRotation3d(); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() {} + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + inverted = invertIMU; + // setOffset(getRawRotation3d()); + } + + private Rotation3d negate(Rotation3d rot) { + return new Rotation3d(-rot.getX(), -rot.getY(), -rot.getZ()); + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRawRotation3d() { + return inverted ? negate(imu.getRotation3d()) : imu.getRotation3d(); + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + return Optional.of( + new Translation3d( + imu.getWorldLinearAccelX(), imu.getWorldLinearAccelY(), imu.getWorldLinearAccelZ()) + .times(9.81)); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated NavX({@link AHRS}) IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/Pigeon2Swerve.java b/src/main/java/swervelib/imu/Pigeon2Swerve.java new file mode 100644 index 00000000..bc5bc2bd --- /dev/null +++ b/src/main/java/swervelib/imu/Pigeon2Swerve.java @@ -0,0 +1,151 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.Pigeon2Configuration; +import com.ctre.phoenix6.configs.Pigeon2Configurator; +import com.ctre.phoenix6.hardware.Pigeon2; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.LinearAcceleration; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; +import java.util.function.Supplier; + +/** SwerveIMU interface for the {@link Pigeon2} */ +public class Pigeon2Swerve extends SwerveIMU { + + /** Wait time for status frames to show up. */ + public static double STATUS_TIMEOUT_SECONDS = 0.04; + /** {@link Pigeon2} IMU device. */ + private final Pigeon2 imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** X Acceleration supplier */ + private final Supplier> xAcc; + /** Y Accelleration supplier. */ + private final Supplier> yAcc; + /** Z Acceleration supplier. */ + private final Supplier> zAcc; + /** Offset for the {@link Pigeon2}. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + /** {@link Pigeon2} configurator. */ + private Pigeon2Configurator cfg; + + /** + * Generate the SwerveIMU for {@link Pigeon2}. + * + * @param canid CAN ID for the {@link Pigeon2} + * @param canbus CAN Bus name the {@link Pigeon2} resides on. + */ + public Pigeon2Swerve(int canid, String canbus) { + imu = new Pigeon2(canid, canbus); + this.cfg = imu.getConfigurator(); + xAcc = imu::getAccelerationX; + yAcc = imu::getAccelerationY; + zAcc = imu::getAccelerationZ; + SmartDashboard.putData(imu); + } + + /** + * Generate the SwerveIMU for {@link Pigeon2}. + * + * @param canid CAN ID for the {@link Pigeon2} + */ + public Pigeon2Swerve(int canid) { + this(canid, ""); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset {@link Pigeon2} to factory default. */ + @Override + public void factoryDefault() { + Pigeon2Configuration config = new Pigeon2Configuration(); + + // Compass utilization causes readings to jump dramatically in some cases. + cfg.apply(config.Pigeon2Features.withEnableCompass(false)); + } + + /** Clear sticky faults on {@link Pigeon2}. */ + @Override + public void clearStickyFaults() { + imu.clearStickyFaults(); + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRawRotation3d() { + Rotation3d reading = imu.getRotation3d(); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + return Optional.of( + new Translation3d( + xAcc.get().getValueAsDouble(), + yAcc.get().getValueAsDouble(), + zAcc.get().getValueAsDouble())); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_replace(imu.getAngularVelocityZWorld().refresh().getValue()); + } + + /** + * Get the instantiated {@link Pigeon2} object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/PigeonSwerve.java b/src/main/java/swervelib/imu/PigeonSwerve.java new file mode 100644 index 00000000..65a44f9f --- /dev/null +++ b/src/main/java/swervelib/imu/PigeonSwerve.java @@ -0,0 +1,121 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import com.ctre.phoenix.sensors.WPI_PigeonIMU; +import edu.wpi.first.math.geometry.Quaternion; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; + +/** SwerveIMU interface for the {@link WPI_PigeonIMU}. */ +public class PigeonSwerve extends SwerveIMU { + + /** {@link WPI_PigeonIMU} IMU device. */ + private final WPI_PigeonIMU imu; + /** Mutable {@link MutAngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** Offset for the {@link WPI_PigeonIMU}. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Generate the SwerveIMU for {@link WPI_PigeonIMU}. + * + * @param canid CAN ID for the {@link WPI_PigeonIMU}, does not support CANBus. + */ + public PigeonSwerve(int canid) { + imu = new WPI_PigeonIMU(canid); + offset = new Rotation3d(); + SmartDashboard.putData(imu); + } + + @Override + public void close() { + imu.close(); + } + + /** Reset IMU to factory default. */ + @Override + public void factoryDefault() { + imu.configFactoryDefault(); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() { + imu.clearStickyFaults(); + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRawRotation3d() { + double[] wxyz = new double[4]; + imu.get6dQuaternion(wxyz); + Rotation3d reading = new Rotation3d(new Quaternion(wxyz[0], wxyz[1], wxyz[2], wxyz[3])); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + short[] initial = new short[3]; + imu.getBiasedAccelerometer(initial); + return Optional.of(new Translation3d(initial[0], initial[1], initial[2]).times(9.81 / 16384.0)); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated {@link WPI_PigeonIMU} IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java b/src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java new file mode 100644 index 00000000..c14d81f8 --- /dev/null +++ b/src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java @@ -0,0 +1,130 @@ +package swervelib.imu; + +import static edu.wpi.first.units.Units.DegreesPerSecond; + +import com.ctre.phoenix.motorcontrol.can.TalonSRX; +import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX; +import com.ctre.phoenix.sensors.WPI_PigeonIMU; +import edu.wpi.first.math.geometry.Quaternion; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.MutAngularVelocity; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.util.Optional; + +/** SwerveIMU interface for the {@link WPI_PigeonIMU}. */ +public class PigeonViaTalonSRXSwerve extends SwerveIMU { + + /** {@link TalonSRX} TalonSRX the IMU is attached to. */ + private final WPI_TalonSRX talon; + + /** {@link WPI_PigeonIMU} IMU device. */ + private final WPI_PigeonIMU imu; + /** Mutable {@link AngularVelocity} for readings. */ + private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); + /** Offset for the {@link WPI_PigeonIMU}. */ + private Rotation3d offset = new Rotation3d(); + /** Inversion for the gyro */ + private boolean invertedIMU = false; + + /** + * Generate the SwerveIMU for {@link WPI_PigeonIMU} attached to a {@link TalonSRX}. + * + * @param canid CAN ID for the {@link TalonSRX} the {@link WPI_PigeonIMU} is attached to, does not + * support CANBus. + */ + public PigeonViaTalonSRXSwerve(int canid) { + talon = new WPI_TalonSRX(canid); + imu = new WPI_PigeonIMU(talon); + offset = new Rotation3d(); + SmartDashboard.putData(imu); + } + + @Override + public void close() { + imu.close(); + talon.close(); + } + + /** Reset IMU to factory default. */ + @Override + public void factoryDefault() { + imu.configFactoryDefault(); + } + + /** Clear sticky faults on IMU. */ + @Override + public void clearStickyFaults() { + imu.clearStickyFaults(); + } + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public void setOffset(Rotation3d offset) { + this.offset = offset; + } + + /** + * Set the gyro to invert its default direction + * + * @param invertIMU invert gyro direction + */ + public void setInverted(boolean invertIMU) { + invertedIMU = invertIMU; + } + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRawRotation3d() { + double[] wxyz = new double[4]; + imu.get6dQuaternion(wxyz); + Rotation3d reading = new Rotation3d(new Quaternion(wxyz[0], wxyz[1], wxyz[2], wxyz[3])); + return invertedIMU ? reading.unaryMinus() : reading; + } + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + @Override + public Rotation3d getRotation3d() { + return getRawRotation3d().rotateBy(offset.unaryMinus()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + @Override + public Optional getAccel() { + short[] initial = new short[3]; + imu.getBiasedAccelerometer(initial); + return Optional.of(new Translation3d(initial[0], initial[1], initial[2]).times(9.81 / 16384.0)); + } + + @Override + public MutAngularVelocity getYawAngularVelocity() { + return yawVel.mut_setMagnitude(imu.getRate()); + } + + /** + * Get the instantiated {@link WPI_PigeonIMU} IMU object. + * + * @return IMU object. + */ + @Override + public Object getIMU() { + return imu; + } +} diff --git a/src/main/java/swervelib/imu/SwerveIMU.java b/src/main/java/swervelib/imu/SwerveIMU.java new file mode 100644 index 00000000..f95661f8 --- /dev/null +++ b/src/main/java/swervelib/imu/SwerveIMU.java @@ -0,0 +1,69 @@ +package swervelib.imu; + +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.units.measure.MutAngularVelocity; +import java.util.Optional; + +/** Swerve IMU abstraction to define a standard interface with a swerve drive. */ +public abstract class SwerveIMU implements AutoCloseable { + + @Override + public abstract void close(); + + /** Reset IMU to factory default. */ + public abstract void factoryDefault(); + + /** Clear sticky faults on IMU. */ + public abstract void clearStickyFaults(); + + /** + * Set the gyro offset. + * + * @param offset gyro offset as a {@link Rotation3d}. + */ + public abstract void setOffset(Rotation3d offset); + + /** + * Set the gyro to invert its default direction. + * + * @param invertIMU gyro direction + */ + public abstract void setInverted(boolean invertIMU); + + /** + * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + public abstract Rotation3d getRawRotation3d(); + + /** + * Fetch the {@link Rotation3d} from the IMU. Robot relative. + * + * @return {@link Rotation3d} from the IMU. + */ + public abstract Rotation3d getRotation3d(); + + /** + * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration + * isn't supported returns empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + public abstract Optional getAccel(); + + /** + * Fetch the rotation rate from the IMU as {@link MutAngularVelocity} + * + * @return {@link MutAngularVelocity} of the rotation rate. + */ + public abstract MutAngularVelocity getYawAngularVelocity(); + + /** + * Get the instantiated IMU object. + * + * @return IMU object. + */ + public abstract Object getIMU(); +} diff --git a/src/main/java/swervelib/imu/package-info.java b/src/main/java/swervelib/imu/package-info.java new file mode 100644 index 00000000..f32f8463 --- /dev/null +++ b/src/main/java/swervelib/imu/package-info.java @@ -0,0 +1,2 @@ +/** IMUs used for controlling the robot heading. All implement {@link swervelib.imu.SwerveIMU}. */ +package swervelib.imu; diff --git a/src/main/java/swervelib/math/Matter.java b/src/main/java/swervelib/math/Matter.java new file mode 100644 index 00000000..8cdca9db --- /dev/null +++ b/src/main/java/swervelib/math/Matter.java @@ -0,0 +1,32 @@ +package swervelib.math; + +import edu.wpi.first.math.geometry.Translation3d; + +/** Object with significant mass that needs to be taken into account. */ +public class Matter { + + /** Position in meters from robot center in 3d space. */ + public Translation3d position; + /** Mass in kg of object. */ + public double mass; + + /** + * Construct an object representing some significant matter on the robot. + * + * @param position Position of the matter in meters. + * @param mass Mass in kg. + */ + public Matter(Translation3d position, double mass) { + this.mass = mass; + this.position = position; + } + + /** + * Get the center mass of the object. + * + * @return center mass = position * mass + */ + public Translation3d massMoment() { + return position.times(mass); + } +} diff --git a/src/main/java/swervelib/math/SwerveMath.java b/src/main/java/swervelib/math/SwerveMath.java new file mode 100644 index 00000000..3c3de296 --- /dev/null +++ b/src/main/java/swervelib/math/SwerveMath.java @@ -0,0 +1,394 @@ +package swervelib.math; + +import edu.wpi.first.math.controller.SimpleMotorFeedforward; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.geometry.Twist2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import java.util.List; +import swervelib.SwerveController; +import swervelib.SwerveModule; +import swervelib.parser.SwerveDriveConfiguration; +import swervelib.parser.SwerveModuleConfiguration; + +/** Mathematical functions which pertain to swerve drive. */ +public class SwerveMath { + + /** + * Calculate the meters per rotation for the integrated encoder. Calculation: (PI * WHEEL DIAMETER + * IN METERS) / (GEAR RATIO * ENCODER RESOLUTION) + * + * @param wheelDiameter Wheel diameter in meters. + * @param driveGearRatio The gear ratio of the drive motor. + * @param pulsePerRotation The number of encoder pulses per rotation. 1 if using an integrated + * encoder. + * @return Meters per rotation for the drive motor. + */ + public static double calculateMetersPerRotation( + double wheelDiameter, double driveGearRatio, double pulsePerRotation) { + return (Math.PI * wheelDiameter) / (driveGearRatio * pulsePerRotation); + } + + /** + * Calculate the meters per rotation for the integrated encoder. Calculation: (PI * WHEEL DIAMETER + * IN METERS) / (GEAR RATIO) + * + * @param wheelDiameter Wheel diameter in meters. + * @param driveGearRatio The gear ratio of the drive motor. + * @return Meters per rotation for the drive motor. + */ + public static double calculateMetersPerRotation(double wheelDiameter, double driveGearRatio) { + return calculateMetersPerRotation(wheelDiameter, driveGearRatio, 1); + } + + /** + * Normalize an angle to be within 0 to 360. + * + * @param angle Angle in degrees. + * @return Normalized angle in degrees. + */ + public static double normalizeAngle(double angle) { + Rotation2d angleRotation = Rotation2d.fromDegrees(angle); + return new Rotation2d(angleRotation.getCos(), angleRotation.getSin()).getDegrees(); + } + + /** + * Algebraically apply a deadband using a piece wise function. + * + * @param value value to apply deadband to. + * @param scaled Use algebra to determine deadband by starting the value at 0 past deadband. + * @param deadband The deadbnad to apply. + * @return Value with deadband applied. + */ + public static double applyDeadband(double value, boolean scaled, double deadband) { + value = Math.abs(value) > deadband ? value : 0; + return scaled + ? ((1 / (1 - deadband)) * (Math.abs(value) - deadband)) * Math.signum(value) + : value; + } + + /** + * Create the drive feedforward for swerve modules. + * + * @param optimalVoltage Optimal voltage to calculate kV (voltage/max Velocity) + * @param maxSpeed Maximum velocity in meters per second to use for the feed forward, should be as + * close to physical max as possible. + * @param wheelGripCoefficientOfFriction Wheel grip coefficient of friction for kA + * (voltage/(cof*9.81)) + * @return Drive feedforward for drive motor on a swerve module. + */ + public static SimpleMotorFeedforward createDriveFeedforward( + double optimalVoltage, double maxSpeed, double wheelGripCoefficientOfFriction) { + double kv = optimalVoltage / maxSpeed; + /// ^ Volt-seconds per meter (max voltage divided by max speed) + double ka = optimalVoltage / calculateMaxAcceleration(wheelGripCoefficientOfFriction); + /// ^ Volt-seconds^2 per meter (max voltage divided by max accel) + return new SimpleMotorFeedforward(0, kv, 0); + } + + /** + * Calculate the degrees per steering rotation for the integrated encoder. Encoder conversion + * values. Drive converts motor rotations to linear wheel distance and steering converts motor + * rotations to module azimuth. + * + * @param angleGearRatio The gear ratio of the steering motor. + * @param pulsePerRotation The number of pulses in a complete rotation for the encoder, 1 if + * integrated. + * @return Degrees per steering rotation for the angle motor. + */ + public static double calculateDegreesPerSteeringRotation( + double angleGearRatio, double pulsePerRotation) { + return 360 / (angleGearRatio * pulsePerRotation); + } + + /** + * Calculate the degrees per steering rotation for the integrated encoder. Encoder conversion + * values. Drive converts motor rotations to linear wheel distance and steering converts motor + * rotations to module azimuth. + * + * @param angleGearRatio The gear ratio of the steering motor. + * @return Degrees per steering rotation for the angle motor. + */ + public static double calculateDegreesPerSteeringRotation(double angleGearRatio) { + return calculateDegreesPerSteeringRotation(angleGearRatio, 1); + } + + /** + * Calculate the maximum angular velocity. + * + * @param maxSpeed Max speed of the robot in meters per second. + * @param furthestModuleX X of the furthest module in meters. + * @param furthestModuleY Y of the furthest module in meters. + * @return Maximum angular velocity in rad/s. + */ + public static double calculateMaxAngularVelocity( + double maxSpeed, double furthestModuleX, double furthestModuleY) { + return maxSpeed / Math.hypot(furthestModuleX, furthestModuleY); + } + + /** + * Calculate the practical maximum acceleration of the robot using the wheel coefficient of + * friction. + * + * @param cof Coefficient of Friction of the wheel grip tape. + * @return Practical maximum acceleration in m/s/s. + */ + public static double calculateMaxAcceleration(double cof) { + return cof * 9.81; + } + + /** + * Calculate the maximum theoretical acceleration without friction. + * + * @param stallTorqueNm Stall torque of driving motor in nM. + * @param gearRatio Gear ratio for driving motor number of motor rotations until one wheel + * rotation. + * @param moduleCount Number of swerve modules. + * @param wheelDiameter Wheel diameter in meters. + * @param robotMass Mass of the robot in kg. + * @return Theoretical maximum acceleration in m/s/s. + */ + public static double calculateMaxAcceleration( + double stallTorqueNm, + double gearRatio, + double moduleCount, + double wheelDiameter, + double robotMass) { + return (stallTorqueNm * gearRatio * moduleCount) / ((wheelDiameter / 2) * robotMass); + } + + /** + * Calculates the maximum acceleration allowed in a direction without tipping the robot. Reads arm + * position from NetworkTables and is passed the direction in question. + * + * @param angle The direction in which to calculate max acceleration, as a Rotation2d. Note that + * this is robot-relative. + * @param matter Matter that the robot is composed of in kg. (Includes chassis) + * @param robotMass The weight of the robot in kg. (Including manipulators, etc). + * @param config The swerve drive configuration. + * @return Maximum acceleration allowed in the robot direction. + */ + private static double calcMaxAccel( + Rotation2d angle, List matter, double robotMass, SwerveDriveConfiguration config) { + // Calculate the vertical mass moment using the floor as the datum. This will be used later to + // calculate max acceleration + Translation3d centerMass = new Translation3d(); + for (Matter object : matter) { + centerMass = centerMass.plus(object.massMoment()); + } + Translation3d robotCG = centerMass.div(robotMass); + Translation2d horizontalCG = robotCG.toTranslation2d(); + + Translation2d projectedHorizontalCg = + new Translation2d( + (angle.getSin() * angle.getCos() * horizontalCG.getY()) + + (Math.pow(angle.getCos(), 2) * horizontalCG.getX()), + (angle.getSin() * angle.getCos() * horizontalCG.getX()) + + (Math.pow(angle.getSin(), 2) * horizontalCG.getY())); + + // Projects the edge of the wheelbase onto the direction line. Assumes the wheelbase is + // rectangular. + // Because a line is being projected, rather than a point, one of the coordinates of the + // projected point is + // already known. + Translation2d projectedWheelbaseEdge; + double angDeg = angle.getDegrees(); + if (angDeg <= 45 && angDeg >= -45) { + SwerveModuleConfiguration conf = getSwerveModule(config.modules, true, true); + projectedWheelbaseEdge = + new Translation2d( + conf.moduleLocation.getX(), conf.moduleLocation.getX() * angle.getTan()); + } else if (135 >= angDeg && angDeg > 45) { + SwerveModuleConfiguration conf = getSwerveModule(config.modules, true, true); + projectedWheelbaseEdge = + new Translation2d( + conf.moduleLocation.getY() / angle.getTan(), conf.moduleLocation.getY()); + } else if (-135 <= angDeg && angDeg < -45) { + SwerveModuleConfiguration conf = getSwerveModule(config.modules, true, false); + projectedWheelbaseEdge = + new Translation2d( + conf.moduleLocation.getY() / angle.getTan(), conf.moduleLocation.getY()); + } else { + SwerveModuleConfiguration conf = getSwerveModule(config.modules, false, true); + projectedWheelbaseEdge = + new Translation2d( + conf.moduleLocation.getX(), conf.moduleLocation.getX() * angle.getTan()); + } + + double horizontalDistance = projectedHorizontalCg.plus(projectedWheelbaseEdge).getNorm(); + return 9.81 * horizontalDistance / robotCG.getZ(); + } + + /** + * Logical inverse of the Pose exponential from 254. Taken from team 3181. + * + * @param transform Pose to perform the log on. + * @return {@link Twist2d} of the transformed pose. + */ + public static Twist2d PoseLog(final Pose2d transform) { + + final double kEps = 1E-9; + final double dtheta = transform.getRotation().getRadians(); + final double half_dtheta = 0.5 * dtheta; + final double cos_minus_one = transform.getRotation().getCos() - 1.0; + double halftheta_by_tan_of_halfdtheta; + if (Math.abs(cos_minus_one) < kEps) { + halftheta_by_tan_of_halfdtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta; + } else { + halftheta_by_tan_of_halfdtheta = + -(half_dtheta * transform.getRotation().getSin()) / cos_minus_one; + } + final Translation2d translation_part = + transform + .getTranslation() + .rotateBy(new Rotation2d(halftheta_by_tan_of_halfdtheta, -half_dtheta)); + return new Twist2d(translation_part.getX(), translation_part.getY(), dtheta); + } + + /** + * Limits a commanded velocity to prevent exceeding the maximum acceleration given by {@link + * SwerveMath#calcMaxAccel}. Note that this takes and returns field-relative velocities. + * + * @param commandedVelocity The desired velocity + * @param fieldVelocity The velocity of the robot within a field relative state. + * @param robotPose The current pose of the robot. + * @param loopTime The time it takes to update the velocity in seconds. Note: this should + * include the 100ms that it takes for a SparkMax velocity to update. + * @param matter Matter that the robot is composed of with position in meters and mass in kg. + * @param robotMass The weight of the robot in kg. (Including manipulators, etc). + * @param config The swerve drive configuration. + * @return The limited velocity. This is either the commanded velocity, if attainable, or the + * closest attainable velocity. + */ + public static Translation2d limitVelocity( + Translation2d commandedVelocity, + ChassisSpeeds fieldVelocity, + Pose2d robotPose, + double loopTime, + double robotMass, + List matter, + SwerveDriveConfiguration config) { + // Get the robot's current field-relative velocity + Translation2d currentVelocity = SwerveController.getTranslation2d(fieldVelocity); + + // Calculate the commanded change in velocity by subtracting current velocity + // from commanded velocity + Translation2d deltaV = commandedVelocity.minus(currentVelocity); + + // Creates an acceleration vector with the direction of delta V and a magnitude + // of the maximum allowed acceleration in that direction + Translation2d maxAccel = + new Translation2d( + calcMaxAccel( + deltaV + // Rotates the velocity vector to convert from field-relative to robot-relative + .rotateBy(robotPose.getRotation().unaryMinus()) + .getAngle(), + matter, + robotMass, + config), + deltaV.getAngle()); + + // Calculate the maximum achievable velocity by the next loop cycle. + // delta V = Vf - Vi = at + Translation2d maxAchievableDeltaVelocity = maxAccel.times(loopTime); + + if (deltaV.getNorm() > maxAchievableDeltaVelocity.getNorm()) { + return maxAchievableDeltaVelocity.plus(currentVelocity); + } else { + // If the commanded velocity is attainable, use that. + return commandedVelocity; + } + } + + /** + * Get the fruthest module from center based on the module locations. + * + * @param modules Swerve module list. + * @param front True = furthest front, False = furthest back. + * @param left True = furthest left, False = furthest right. + * @return Module location which is the furthest from center and abides by parameters. + */ + public static SwerveModuleConfiguration getSwerveModule( + SwerveModule[] modules, boolean front, boolean left) { + Translation2d target = modules[0].configuration.moduleLocation, current, temp; + SwerveModuleConfiguration configuration = modules[0].configuration; + for (SwerveModule module : modules) { + current = module.configuration.moduleLocation; + temp = + front + ? (target.getY() >= current.getY() ? current : target) + : (target.getY() <= current.getY() ? current : target); + target = + left + ? (target.getX() >= temp.getX() ? temp : target) + : (target.getX() <= temp.getX() ? temp : target); + configuration = current.equals(target) ? module.configuration : configuration; + } + return configuration; + } + + /** + * Put an angle within the 360 deg scope of a reference. For example, given a scope reference of + * 756 degrees, assumes the full scope is (720-1080), and places an angle of 22 degrees into it, + * returning 742 deg. + * + *

A more formal definition: returns the closest angle {@code n} to {@code scopeReference} such + * that {@code n} is congruent to {@code newAngle}. + * + * @param scopeReference Current Angle (deg) + * @param newAngle Target Angle (deg) + * @return Closest angle within scope (deg) + */ + public static double placeInAppropriate0To360Scope(double scopeReference, double newAngle) { + // Figure out how many revolutions from the angle to the reference + double diffRevs = Math.round((scopeReference - newAngle) / 360) * 360; + + // Add that many revolutions + return diffRevs + newAngle; + } + + /** + * Perform anti-jitter within modules if the speed requested is too low. + * + * @param moduleState Current {@link SwerveModuleState} requested. + * @param lastModuleState Previous {@link SwerveModuleState} used. + * @param maxSpeed Maximum speed of the modules. + */ + public static void antiJitter( + SwerveModuleState moduleState, SwerveModuleState lastModuleState, double maxSpeed) { + if (Math.abs(moduleState.speedMetersPerSecond) <= (maxSpeed * 0.01)) { + moduleState.angle = lastModuleState.angle; + } + } + + /** + * Cube the {@link Translation2d} magnitude given in Polar coordinates. + * + * @param translation {@link Translation2d} to manipulate. + * @return Cubed magnitude from {@link Translation2d}. + */ + public static Translation2d cubeTranslation(Translation2d translation) { + if (Math.hypot(translation.getX(), translation.getY()) <= 1.0E-6) { + return translation; + } + return new Translation2d(Math.pow(translation.getNorm(), 3), translation.getAngle()); + } + + /** + * Scale the {@link Translation2d} Polar coordinate magnitude. + * + * @param translation {@link Translation2d} to use. + * @param scalar Multiplier for the Polar coordinate magnitude to use. + * @return {@link Translation2d} scaled by given magnitude scalar. + */ + public static Translation2d scaleTranslation(Translation2d translation, double scalar) { + if (Math.hypot(translation.getX(), translation.getY()) <= 1.0E-6) { + return translation; + } + return new Translation2d(translation.getNorm() * scalar, translation.getAngle()); + } +} diff --git a/src/main/java/swervelib/math/package-info.java b/src/main/java/swervelib/math/package-info.java new file mode 100644 index 00000000..633b280c --- /dev/null +++ b/src/main/java/swervelib/math/package-info.java @@ -0,0 +1,6 @@ +/** + * Mathematics for swerve drives. Original second order kinematics was developed by Team 3181 here. + * + */ +package swervelib.math; diff --git a/src/main/java/swervelib/motors/SparkFlexSwerve.java b/src/main/java/swervelib/motors/SparkFlexSwerve.java new file mode 100644 index 00000000..03b2e9ae --- /dev/null +++ b/src/main/java/swervelib/motors/SparkFlexSwerve.java @@ -0,0 +1,455 @@ +package swervelib.motors; + +import static edu.wpi.first.units.Units.Milliseconds; +import static edu.wpi.first.units.Units.Seconds; + +import com.revrobotics.AbsoluteEncoder; +import com.revrobotics.REVLibError; +import com.revrobotics.RelativeEncoder; +import com.revrobotics.spark.ClosedLoopSlot; +import com.revrobotics.spark.SparkBase.ControlType; +import com.revrobotics.spark.SparkBase.PersistMode; +import com.revrobotics.spark.SparkBase.ResetMode; +import com.revrobotics.spark.SparkClosedLoopController; +import com.revrobotics.spark.SparkFlex; +import com.revrobotics.spark.SparkLowLevel.MotorType; +import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; +import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; +import com.revrobotics.spark.config.SparkFlexConfig; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Timer; +import java.util.Optional; +import java.util.function.Supplier; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** An implementation of {@link SparkFlex} as a {@link SwerveMotor}. */ +public class SparkFlexSwerve extends SwerveMotor { + + /** Config retry delay. */ + private final double configDelay = Milliseconds.of(5).in(Seconds); + /** {@link SparkFlex} Instance. */ + private final SparkFlex motor; + /** Integrated encoder. */ + public RelativeEncoder encoder; + /** Absolute encoder attached to the SparkFlex (if exists) */ + public Optional absoluteEncoder = Optional.empty(); + /** Closed-loop PID controller. */ + public SparkClosedLoopController pid; + /** Supplier for the velocity of the motor controller. */ + private Supplier velocity; + /** Supplier for the position of the motor controller. */ + private Supplier position; + /** An {@link Alert} for if there is an error configuring the motor. */ + private Alert failureConfiguring; + /** Configuration object for {@link SparkFlex} motor. */ + private SparkFlexConfig cfg = new SparkFlexConfig(); + + /** + * Initialize the swerve motor. + * + * @param motor The SwerveMotor as a SparkFlex object. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param motorType {@link DCMotor} which the {@link SparkFlex} is attached to. + */ + public SparkFlexSwerve(SparkFlex motor, boolean isDriveMotor, DCMotor motorType) { + this.motor = motor; + this.isDriveMotor = isDriveMotor; + failureConfiguring = + new Alert("Motors", "Failure configuring motor " + motor.getDeviceId(), AlertType.kWarning); + factoryDefaults(); + clearStickyFaults(); + + encoder = motor.getEncoder(); + pid = motor.getClosedLoopController(); + cfg.closedLoop.feedbackSensor( + FeedbackSensor + .kPrimaryEncoder); // Configure feedback of the PID controller as the integrated + // encoder. + + // Spin off configurations in a different thread. + // configureSparkFlex(() -> motor.setCANTimeout(0)); // Commented out because it prevents + // feedback. + + velocity = encoder::getVelocity; + position = encoder::getPosition; + } + + /** + * Initialize the {@link SwerveMotor} as a {@link SparkFlex} connected to a Brushless Motor. + * + * @param id CAN ID of the SparkFlex. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param motorType {@link DCMotor} which the {@link SparkFlex} is attached to. + */ + public SparkFlexSwerve(int id, boolean isDriveMotor, DCMotor motorType) { + this(new SparkFlex(id, MotorType.kBrushless), isDriveMotor, motorType); + } + + /** + * Run the configuration until it succeeds or times out. + * + * @param config Lambda supplier returning the error state. + */ + private void configureSparkFlex(Supplier config) { + for (int i = 0; i < maximumRetries; i++) { + if (config.get() == REVLibError.kOk) { + return; + } + Timer.delay(configDelay); + } + failureConfiguring.set(true); + } + + @Override + public void close() { + motor.close(); + } + + /** + * Get the current configuration of the {@link SparkFlex} + * + * @return {@link SparkFlexConfig} + */ + public SparkFlexConfig getConfig() { + return cfg; + } + + /** + * Update the config for the {@link SparkFlex} + * + * @param cfgGiven Given {@link SparkFlexConfig} which should have minimal modifications. + */ + public void updateConfig(SparkFlexConfig cfgGiven) { + if (!DriverStation.isDisabled()) { + DriverStation.reportWarning( + "Configuration changes cannot be applied while the robot is enabled.", false); + } + cfg.apply(cfgGiven); + configureSparkFlex( + () -> + motor.configure(cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters)); + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + cfg.voltageCompensation(nominalVoltage); + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + + cfg.smartCurrentLimit(currentLimit); + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + cfg.closedLoopRampRate(rampRate).openLoopRampRate(rampRate); + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + @Override + public DCMotor getSimMotor() { + if (simMotor == null) { + simMotor = DCMotor.getNeoVortex(1); + } + return simMotor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder.isPresent(); + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + // Do nothing + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + configureSparkFlex(motor::clearFaults); + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + * @return The {@link SwerveMotor} for easy instantiation. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + if (encoder == null) { + this.absoluteEncoder = Optional.empty(); + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + + velocity = this.encoder::getVelocity; + position = this.encoder::getPosition; + } else if (encoder.getAbsoluteEncoder() instanceof AbsoluteEncoder) { + cfg.closedLoop.feedbackSensor(FeedbackSensor.kAbsoluteEncoder); + this.absoluteEncoder = Optional.of(encoder); + + velocity = this.absoluteEncoder.get()::getVelocity; + position = this.absoluteEncoder.get()::getAbsolutePosition; + } + return this; + } + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply. + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + cfg.signals + .absoluteEncoderPositionAlwaysOn(false) + .absoluteEncoderVelocityAlwaysOn(false) + .analogPositionAlwaysOn(false) + .analogVelocityAlwaysOn(false) + .analogVoltageAlwaysOn(false) + .externalOrAltEncoderPositionAlwaysOn(false) + .externalOrAltEncoderVelocityAlwaysOn(false) + .primaryEncoderPositionAlwaysOn(false) + .primaryEncoderVelocityAlwaysOn(false) + .iAccumulationAlwaysOn(false) + .appliedOutputPeriodMs(10) + .faultsPeriodMs(20); + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + + cfg.encoder + .positionConversionFactor(positionConversionFactor) + .velocityConversionFactor(positionConversionFactor / 60); + // Changes the measurement period and number of samples used to calculate the velocity for the + // intergrated motor controller + // Notability this changes the returned velocity and the velocity used for the onboard velocity + // PID loop (TODO: triple check the PID portion of this statement) + // Default settings of 32ms and 8 taps introduce ~100ms of measurement lag + // https://www.chiefdelphi.com/t/shooter-encoder/400211/11 + // This value was taken from: + // https://github.com/Mechanical-Advantage/RobotCode2023/blob/9884d13b2220b76d430e82248fd837adbc4a10bc/src/main/java/org/littletonrobotics/frc2023/subsystems/drive/ModuleIOSparkMax.java#L132-L133 + // and tested on 9176 for YAGSL, notably 3005 uses 16ms instead of 10 but 10 is more common + // based on github searches + cfg.encoder.quadratureMeasurementPeriod(10).quadratureAverageDepth(2); + + // Taken from + // https://github.com/frc3512/SwerveBot-2022/blob/9d31afd05df6c630d5acb4ec2cf5d734c9093bf8/src/main/java/frc/lib/util/SparkMaxUtil.java#L67 + // Unused frames can be set to 65535 to decrease CAN ultilization. + cfg.signals + .primaryEncoderVelocityAlwaysOn( + isDriveMotor) // Disable velocity reporting for angle motors. + .primaryEncoderPositionAlwaysOn(true) + .primaryEncoderPositionPeriodMs(20); + } + + /** + * Configure the PIDF values for the closed loop controller. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + cfg.closedLoop + .pidf(config.p, config.i, config.d, config.f) + .iZone(config.iz) + .outputRange(config.output.min, config.output.max); + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + cfg.closedLoop.positionWrappingEnabled(true).positionWrappingInputRange(minInput, maxInput); + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + cfg.closedLoop.positionWrappingEnabled(false); + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + cfg.idleMode(isBrakeMode ? IdleMode.kBrake : IdleMode.kCoast); + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + cfg.inverted(inverted); + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + configureSparkFlex( + () -> { + return motor.configure( + cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters); + }); + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + + if (isDriveMotor) { + configureSparkFlex( + () -> + pid.setReference( + setpoint, ControlType.kVelocity, ClosedLoopSlot.kSlot0, feedforward)); + } else { + configureSparkFlex( + () -> + pid.setReference( + setpoint, ControlType.kPosition, ClosedLoopSlot.kSlot0, feedforward)); + if (SwerveDriveTelemetry.isSimulation) { + encoder.setPosition(setpoint); + } + } + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + setReference(setpoint, feedforward); + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getAppliedOutput() * motor.getBusVoltage(); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + @Override + public double getAppliedOutput() { + return motor.getAppliedOutput(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity + */ + @Override + public double getVelocity() { + return velocity.get(); + } + + /** + * Get the position of the integrated encoder. + * + * @return Position + */ + @Override + public double getPosition() { + return position.get(); + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. + */ + @Override + public void setPosition(double position) { + if (absoluteEncoder.isEmpty()) { + configureSparkFlex(() -> encoder.setPosition(position)); + } + } +} diff --git a/src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java b/src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java new file mode 100644 index 00000000..98b73b30 --- /dev/null +++ b/src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java @@ -0,0 +1,555 @@ +package swervelib.motors; + +import static edu.wpi.first.units.Units.Milliseconds; +import static edu.wpi.first.units.Units.Seconds; + +import com.revrobotics.REVLibError; +import com.revrobotics.RelativeEncoder; +import com.revrobotics.spark.ClosedLoopSlot; +import com.revrobotics.spark.SparkBase.ControlType; +import com.revrobotics.spark.SparkBase.PersistMode; +import com.revrobotics.spark.SparkBase.ResetMode; +import com.revrobotics.spark.SparkClosedLoopController; +import com.revrobotics.spark.SparkLowLevel.MotorType; +import com.revrobotics.spark.SparkMax; +import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; +import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; +import com.revrobotics.spark.config.SparkMaxConfig; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.Timer; +import java.util.Optional; +import java.util.function.Supplier; +import swervelib.encoders.SparkMaxAnalogEncoderSwerve; +import swervelib.encoders.SparkMaxEncoderSwerve; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** Brushed motor control with {@link SparkMax}. */ +public class SparkMaxBrushedMotorSwerve extends SwerveMotor { + + /** Config retry delay. */ + private final double configDelay = Milliseconds.of(5).in(Seconds); + /** SparkMAX Instance. */ + private final SparkMax motor; + /** Absolute encoder attached to the SparkMax (if exists) */ + public Optional absoluteEncoder = Optional.empty(); + /** Integrated encoder. */ + public Optional encoder = Optional.empty(); + /** Closed-loop PID controller. */ + public SparkClosedLoopController pid; + /** Supplier for the velocity of the motor controller. */ + private Supplier velocity; + /** Supplier for the position of the motor controller. */ + private Supplier position; + /** An {@link Alert} for if the motor has no encoder. */ + private Alert noEncoderAlert; + /** An {@link Alert} for if there is an error configuring the motor. */ + private Alert failureConfiguringAlert; + /** An {@link Alert} for if the motor has no encoder defined. */ + private Alert noEncoderDefinedAlert; + /** Configuration object for {@link SparkMax} motor. */ + private SparkMaxConfig cfg = new SparkMaxConfig(); + + /** + * Initialize the swerve motor. + * + * @param motor The SwerveMotor as a SparkMax object. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param encoderType {@link Type} of encoder to use for the {@link SparkMax} device. + * @param countsPerRevolution The number of encoder pulses for the {@link Type} encoder per + * revolution. + * @param useDataPortQuadEncoder Use the encoder attached to the data port of the spark max for a + * quadrature encoder. + * @param motorType {@link DCMotor} which the {@link SparkMax} is attached to. + */ + public SparkMaxBrushedMotorSwerve( + SparkMax motor, + boolean isDriveMotor, + Type encoderType, + int countsPerRevolution, + boolean useDataPortQuadEncoder, + DCMotor motorType) { + noEncoderAlert = new Alert("Motors", "Cannot use motor without encoder.", AlertType.kError); + failureConfiguringAlert = + new Alert("Motors", "Failure configuring motor " + motor.getDeviceId(), AlertType.kWarning); + noEncoderDefinedAlert = + new Alert("Motors", "An encoder MUST be defined to work with a SparkMAX", AlertType.kError); + + // Drive motors **MUST** have an encoder attached. + if (isDriveMotor && encoderType == Type.kNoSensor) { + noEncoderAlert.set(true); + throw new RuntimeException( + "Cannot use SparkMAX as a drive motor without an encoder attached."); + } + + // Hall encoders can be used as quadrature encoders. + if (encoderType == Type.kHallSensor) { + encoderType = Type.kQuadrature; + } + + this.motor = motor; + this.isDriveMotor = isDriveMotor; + this.simMotor = motorType; + + factoryDefaults(); + clearStickyFaults(); + + // Get the onboard PID controller. + pid = motor.getClosedLoopController(); + + // If there is a sensor attached to the data port or encoder port set the relative encoder. + if (isDriveMotor || (encoderType != Type.kNoSensor || useDataPortQuadEncoder)) { + + if (useDataPortQuadEncoder) { + this.encoder = Optional.of(motor.getAlternateEncoder()); + cfg.alternateEncoder.countsPerRevolution(countsPerRevolution); + + // Configure feedback of the PID controller as the integrated encoder. + cfg.closedLoop.feedbackSensor(FeedbackSensor.kAlternateOrExternalEncoder); + } else { + this.encoder = Optional.of(motor.getEncoder()); + cfg.encoder.countsPerRevolution(countsPerRevolution); + + // Configure feedback of the PID controller as the integrated encoder. + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + } + } + encoder.ifPresentOrElse( + (RelativeEncoder enc) -> { + velocity = enc::getVelocity; + position = enc::getPosition; + }, + () -> { + noEncoderDefinedAlert.set(true); + }); + // Spin off configurations in a different thread. + // configureSparkMax(() -> motor.setCANTimeout(0)); // Commented it out because it prevents + // feedback. + + } + + /** + * Initialize the {@link SwerveMotor} as a {@link SparkMax} connected to a Brushless Motor. + * + * @param id CAN ID of the SparkMax. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param encoderType {@link Type} of encoder to use for the {@link SparkMax} device. + * @param countsPerRevolution The number of encoder pulses for the {@link Type} encoder per + * revolution. + * @param useDataPortQuadEncoder Use the encoder attached to the data port of the spark max for a + * quadrature encoder. + * @param motorType Motor type controlled by the {@link SparkMax} motor controller. + */ + public SparkMaxBrushedMotorSwerve( + int id, + boolean isDriveMotor, + Type encoderType, + int countsPerRevolution, + boolean useDataPortQuadEncoder, + DCMotor motorType) { + this( + new SparkMax(id, MotorType.kBrushed), + isDriveMotor, + encoderType, + countsPerRevolution, + useDataPortQuadEncoder, + motorType); + } + + /** + * Run the configuration until it succeeds or times out. + * + * @param config Lambda supplier returning the error state. + */ + private void configureSparkMax(Supplier config) { + for (int i = 0; i < maximumRetries; i++) { + if (config.get() == REVLibError.kOk) { + return; + } + Timer.delay(configDelay); + } + failureConfiguringAlert.set(true); + } + + @Override + public void close() { + motor.close(); + } + + /** + * Get the current configuration of the {@link SparkMax} + * + * @return {@link SparkMaxConfig} + */ + public SparkMaxConfig getConfig() { + return cfg; + } + + /** + * Update the config for the {@link SparkMax} + * + * @param cfgGiven Given {@link SparkMaxConfig} which should have minimal modifications. + */ + public void updateConfig(SparkMaxConfig cfgGiven) { + cfg.apply(cfgGiven); + configureSparkMax( + () -> + motor.configure(cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters)); + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + cfg.voltageCompensation(nominalVoltage); + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + cfg.smartCurrentLimit(currentLimit); + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + cfg.closedLoopRampRate(rampRate).openLoopRampRate(rampRate); + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + @Override + public DCMotor getSimMotor() { + if (simMotor == null) { + simMotor = DCMotor.getCIM(1); + } + return simMotor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder.isPresent(); + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + // Do nothing + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + configureSparkMax(motor::clearFaults); + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + * @return The {@link SwerveMotor} for easy instantiation. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + if (encoder == null) { + this.absoluteEncoder = Optional.empty(); + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + + this.encoder.ifPresentOrElse( + (RelativeEncoder enc) -> { + velocity = enc::getVelocity; + position = enc::getPosition; + }, + () -> { + noEncoderDefinedAlert.set(true); + }); + burnFlash(); + } else if (encoder instanceof SparkMaxAnalogEncoderSwerve + || encoder instanceof SparkMaxEncoderSwerve) { + cfg.closedLoop.feedbackSensor( + encoder instanceof SparkMaxAnalogEncoderSwerve + ? FeedbackSensor.kAnalogSensor + : FeedbackSensor.kAbsoluteEncoder); + + this.absoluteEncoder = Optional.of(encoder); + velocity = this.absoluteEncoder.get()::getVelocity; + position = this.absoluteEncoder.get()::getAbsolutePosition; + noEncoderDefinedAlert.set(false); + } + if (absoluteEncoder.isEmpty() && this.encoder.isEmpty()) { + noEncoderDefinedAlert.set(true); + throw new RuntimeException("An encoder MUST be defined to work with a SparkMAX"); + } + return this; + } + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply. + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + cfg.signals + .absoluteEncoderPositionAlwaysOn(false) + .absoluteEncoderVelocityAlwaysOn(false) + .analogPositionAlwaysOn(false) + .analogVelocityAlwaysOn(false) + .analogVoltageAlwaysOn(false) + .externalOrAltEncoderPositionAlwaysOn(false) + .externalOrAltEncoderVelocityAlwaysOn(false) + .primaryEncoderPositionAlwaysOn(false) + .primaryEncoderVelocityAlwaysOn(false) + .iAccumulationAlwaysOn(false) + .appliedOutputPeriodMs(10) + .faultsPeriodMs(20); + + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + cfg.encoder + .positionConversionFactor(positionConversionFactor) + .velocityConversionFactor(positionConversionFactor / 60); + // Changes the measurement period and number of samples used to calculate the velocity for the + // intergrated motor controller + // Notability this changes the returned velocity and the velocity used for the onboard velocity + // PID loop (TODO: triple check the PID portion of this statement) + // Default settings of 32ms and 8 taps introduce ~100ms of measurement lag + // https://www.chiefdelphi.com/t/shooter-encoder/400211/11 + // This value was taken from: + // https://github.com/Mechanical-Advantage/RobotCode2023/blob/9884d13b2220b76d430e82248fd837adbc4a10bc/src/main/java/org/littletonrobotics/frc2023/subsystems/drive/ModuleIOSparkMax.java#L132-L133 + // and tested on 9176 for YAGSL, notably 3005 uses 16ms instead of 10 but 10 is more common + // based on github searches + cfg.encoder.quadratureMeasurementPeriod(10).quadratureAverageDepth(2); + + // Taken from + // https://github.com/frc3512/SwerveBot-2022/blob/9d31afd05df6c630d5acb4ec2cf5d734c9093bf8/src/main/java/frc/lib/util/SparkMaxUtil.java#L67 + // Unused frames can be set to 65535 to decrease CAN ultilization. + cfg.signals + .primaryEncoderVelocityAlwaysOn( + isDriveMotor) // Disable velocity reporting for angle motors. + .primaryEncoderPositionAlwaysOn(true) + .primaryEncoderPositionPeriodMs(20); + } + + /** + * Configure the PIDF values for the closed loop controller. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + cfg.closedLoop + .pidf(config.p, config.i, config.d, config.f) + .iZone(config.iz) + .outputRange(config.output.min, config.output.max); + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + cfg.closedLoop.positionWrappingEnabled(true).positionWrappingInputRange(minInput, maxInput); + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + cfg.closedLoop.positionWrappingEnabled(false); + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + cfg.idleMode(isBrakeMode ? IdleMode.kBrake : IdleMode.kCoast); + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + cfg.inverted(inverted); + if (isDriveMotor) { + cfg.encoder.inverted(inverted); + } + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + configureSparkMax( + () -> { + return motor.configure( + cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters); + }); + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + int pidSlot = 0; + + if (isDriveMotor) { + configureSparkMax( + () -> + pid.setReference( + setpoint, ControlType.kVelocity, ClosedLoopSlot.kSlot0, feedforward)); + } else { + configureSparkMax( + () -> + pid.setReference( + setpoint, ControlType.kPosition, ClosedLoopSlot.kSlot0, feedforward)); + if (SwerveDriveTelemetry.isSimulation) { + encoder.ifPresent( + (RelativeEncoder enc) -> { + enc.setPosition(setpoint); + }); + } + } + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + setReference(setpoint, feedforward); + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getAppliedOutput() * motor.getBusVoltage(); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + @Override + public double getAppliedOutput() { + return motor.getAppliedOutput(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity + */ + @Override + public double getVelocity() { + return velocity.get(); + } + + /** + * Get the position of the integrated encoder. + * + * @return Position + */ + @Override + public double getPosition() { + return position.get(); + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. + */ + @Override + public void setPosition(double position) { + if (absoluteEncoder.isEmpty()) { + encoder.ifPresent( + (RelativeEncoder enc) -> { + configureSparkMax(() -> enc.setPosition(position)); + }); + } + } + + /** Type for encoder for {@link SparkMax} */ + public enum Type { + /** NO sensor */ + kNoSensor, + /** Hall sensor attached to dataport */ + kHallSensor, + /** Quad encoder attached to alt */ + kQuadrature, + } +} diff --git a/src/main/java/swervelib/motors/SparkMaxSwerve.java b/src/main/java/swervelib/motors/SparkMaxSwerve.java new file mode 100644 index 00000000..e70e12c4 --- /dev/null +++ b/src/main/java/swervelib/motors/SparkMaxSwerve.java @@ -0,0 +1,456 @@ +package swervelib.motors; + +import static edu.wpi.first.units.Units.Milliseconds; +import static edu.wpi.first.units.Units.Seconds; + +import com.revrobotics.REVLibError; +import com.revrobotics.RelativeEncoder; +import com.revrobotics.spark.ClosedLoopSlot; +import com.revrobotics.spark.SparkBase.ControlType; +import com.revrobotics.spark.SparkBase.PersistMode; +import com.revrobotics.spark.SparkBase.ResetMode; +import com.revrobotics.spark.SparkClosedLoopController; +import com.revrobotics.spark.SparkLowLevel.MotorType; +import com.revrobotics.spark.SparkMax; +import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; +import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; +import com.revrobotics.spark.config.SparkMaxConfig; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.Timer; +import java.util.Optional; +import java.util.function.Supplier; +import swervelib.encoders.SparkMaxAnalogEncoderSwerve; +import swervelib.encoders.SparkMaxEncoderSwerve; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** An implementation of {@link com.revrobotics.spark.SparkMax} as a {@link SwerveMotor}. */ +public class SparkMaxSwerve extends SwerveMotor { + + /** Config retry delay. */ + private final double configDelay = Milliseconds.of(5).in(Seconds); + /** {@link SparkMax} Instance. */ + private final SparkMax motor; + /** Integrated encoder. */ + public RelativeEncoder encoder; + /** Closed-loop PID controller. */ + public SparkClosedLoopController pid; + /** Absolute encoder attached to the SparkMax (if exists) */ + private Optional absoluteEncoder = Optional.empty(); + /** Supplier for the velocity of the motor controller. */ + private Supplier velocity; + /** Supplier for the position of the motor controller. */ + private Supplier position; + /** Configuration object for {@link SparkMax} motor. */ + private SparkMaxConfig cfg = new SparkMaxConfig(); + + /** + * Initialize the swerve motor. + * + * @param motor The SwerveMotor as a SparkMax object. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param motorType Motor type controlled by the {@link SparkMax} motor controller. + */ + public SparkMaxSwerve(SparkMax motor, boolean isDriveMotor, DCMotor motorType) { + this.motor = motor; + this.isDriveMotor = isDriveMotor; + this.simMotor = motorType; + factoryDefaults(); + clearStickyFaults(); + + encoder = motor.getEncoder(); + pid = motor.getClosedLoopController(); + + cfg.closedLoop.feedbackSensor( + FeedbackSensor + .kPrimaryEncoder); // Configure feedback of the PID controller as the integrated + // encoder. + velocity = encoder::getVelocity; + position = encoder::getPosition; + + // Spin off configurations in a different thread. + // configureSparkMax(() -> motor.setCANTimeout(0)); // Commented out because it prevents + // feedback. + } + + /** + * Initialize the {@link SwerveMotor} as a {@link SparkMax} connected to a Brushless Motor. + * + * @param id CAN ID of the SparkMax. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param motorType Motor type controlled by the {@link SparkMax} motor controller. + */ + public SparkMaxSwerve(int id, boolean isDriveMotor, DCMotor motorType) { + this(new SparkMax(id, MotorType.kBrushless), isDriveMotor, motorType); + } + + /** + * Run the configuration until it succeeds or times out. + * + * @param config Lambda supplier returning the error state. + */ + private void configureSparkMax(Supplier config) { + for (int i = 0; i < maximumRetries; i++) { + if (config.get() == REVLibError.kOk) { + return; + } + Timer.delay(configDelay); + } + DriverStation.reportWarning("Failure configuring motor " + motor.getDeviceId(), true); + } + + @Override + public void close() { + motor.close(); + } + + /** + * Get the current configuration of the {@link SparkMax} + * + * @return {@link SparkMaxConfig} + */ + public SparkMaxConfig getConfig() { + return cfg; + } + + /** + * Update the config for the {@link SparkMax} + * + * @param cfgGiven Given {@link SparkMaxConfig} which should have minimal modifications. + */ + public void updateConfig(SparkMaxConfig cfgGiven) { + if (!DriverStation.isDisabled()) { + DriverStation.reportWarning( + "Configuration changes cannot be applied while the robot is enabled.", false); + } + cfg.apply(cfgGiven); + configureSparkMax( + () -> + motor.configure(cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters)); + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + cfg.voltageCompensation(nominalVoltage); + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + cfg.smartCurrentLimit(currentLimit); + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + cfg.closedLoopRampRate(rampRate).openLoopRampRate(rampRate); + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + @Override + public DCMotor getSimMotor() { + if (simMotor == null) { + simMotor = DCMotor.getNEO(1); + } + return simMotor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder.isPresent(); + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + // Do nothing + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + configureSparkMax(motor::clearFaults); + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + * @return The {@link SwerveMotor} for easy instantiation. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + if (encoder == null) { + this.absoluteEncoder = Optional.empty(); + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + + velocity = this.encoder::getVelocity; + position = this.encoder::getPosition; + burnFlash(); + } else if (encoder instanceof SparkMaxAnalogEncoderSwerve + || encoder instanceof SparkMaxEncoderSwerve) { + cfg.closedLoop.feedbackSensor( + encoder instanceof SparkMaxAnalogEncoderSwerve + ? FeedbackSensor.kAnalogSensor + : FeedbackSensor.kAbsoluteEncoder); + + this.absoluteEncoder = Optional.of(encoder); + velocity = this.absoluteEncoder.get()::getVelocity; + position = this.absoluteEncoder.get()::getAbsolutePosition; + } + return this; + } + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply. + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + cfg.signals + .absoluteEncoderPositionAlwaysOn(false) + .absoluteEncoderVelocityAlwaysOn(false) + .analogPositionAlwaysOn(false) + .analogVelocityAlwaysOn(false) + .analogVoltageAlwaysOn(false) + .externalOrAltEncoderPositionAlwaysOn(false) + .externalOrAltEncoderVelocityAlwaysOn(false) + .primaryEncoderPositionAlwaysOn(false) + .primaryEncoderVelocityAlwaysOn(false) + .iAccumulationAlwaysOn(false) + .appliedOutputPeriodMs(10) + .faultsPeriodMs(20); + + cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); + cfg.encoder + .positionConversionFactor(positionConversionFactor) + .velocityConversionFactor(positionConversionFactor / 60); + // Changes the measurement period and number of samples used to calculate the velocity for the + // intergrated motor controller + // Notability this changes the returned velocity and the velocity used for the onboard velocity + // PID loop (TODO: triple check the PID portion of this statement) + // Default settings of 32ms and 8 taps introduce ~100ms of measurement lag + // https://www.chiefdelphi.com/t/shooter-encoder/400211/11 + // This value was taken from: + // https://github.com/Mechanical-Advantage/RobotCode2023/blob/9884d13b2220b76d430e82248fd837adbc4a10bc/src/main/java/org/littletonrobotics/frc2023/subsystems/drive/ModuleIOSparkMax.java#L132-L133 + // and tested on 9176 for YAGSL, notably 3005 uses 16ms instead of 10 but 10 is more common + // based on github searches + cfg.encoder.quadratureMeasurementPeriod(10).quadratureAverageDepth(2); + + // Taken from + // https://github.com/frc3512/SwerveBot-2022/blob/9d31afd05df6c630d5acb4ec2cf5d734c9093bf8/src/main/java/frc/lib/util/SparkMaxUtil.java#L67 + // Unused frames can be set to 65535 to decrease CAN ultilization. + cfg.signals + .primaryEncoderVelocityAlwaysOn( + isDriveMotor) // Disable velocity reporting for angle motors. + .primaryEncoderPositionAlwaysOn(true) + .primaryEncoderPositionPeriodMs(20); + } + + /** + * Configure the PIDF values for the closed loop controller. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + cfg.closedLoop + .pidf(config.p, config.i, config.d, config.f) + .iZone(config.iz) + .outputRange(config.output.min, config.output.max); + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + cfg.closedLoop.positionWrappingEnabled(true).positionWrappingInputRange(minInput, maxInput); + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + cfg.closedLoop.positionWrappingEnabled(false); + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + cfg.idleMode(isBrakeMode ? IdleMode.kBrake : IdleMode.kCoast); + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + cfg.inverted(inverted); + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + configureSparkMax( + () -> { + return motor.configure( + cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters); + }); + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + int pidSlot = 0; + + if (isDriveMotor) { + configureSparkMax( + () -> + pid.setReference( + setpoint, ControlType.kVelocity, ClosedLoopSlot.kSlot0, feedforward)); + } else { + configureSparkMax( + () -> + pid.setReference( + setpoint, ControlType.kPosition, ClosedLoopSlot.kSlot0, feedforward)); + if (SwerveDriveTelemetry.isSimulation) { + encoder.setPosition(setpoint); + } + } + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + setReference(setpoint, feedforward); + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getAppliedOutput() * motor.getBusVoltage(); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + @Override + public double getAppliedOutput() { + return motor.getAppliedOutput(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity + */ + @Override + public double getVelocity() { + return velocity.get(); + } + + /** + * Get the position of the integrated encoder. + * + * @return Position + */ + @Override + public double getPosition() { + return position.get(); + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. + */ + @Override + public void setPosition(double position) { + if (absoluteEncoder.isEmpty()) { + configureSparkMax(() -> encoder.setPosition(position)); + } + } +} diff --git a/src/main/java/swervelib/motors/SwerveMotor.java b/src/main/java/swervelib/motors/SwerveMotor.java new file mode 100644 index 00000000..d139d36a --- /dev/null +++ b/src/main/java/swervelib/motors/SwerveMotor.java @@ -0,0 +1,195 @@ +package swervelib.motors; + +import edu.wpi.first.math.system.plant.DCMotor; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; + +/** + * Swerve motor abstraction which defines a standard interface for motors within a swerve module. + */ +public abstract class SwerveMotor implements AutoCloseable { + + @Override + public abstract void close(); + + /** + * The maximum amount of times the swerve motor will attempt to configure a motor if failures + * occur. + */ + public final int maximumRetries = 5; + /** + * Sim motor to use, defaulted in {@link SwerveMotor#getSimMotor()}, but can be overridden here. + *
+ * NOTE: This will not change the simulation motor type! It is intended for use only if you are + * utilizing Feedforwards from PathPlanner. + */ + public DCMotor simMotor; + /** Whether the swerve motor is a drive motor. */ + protected boolean isDriveMotor; + + /** Configure the factory defaults. */ + public abstract void factoryDefaults(); + + /** Clear the sticky faults on the motor controller. */ + public abstract void clearStickyFaults(); + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + * @return The {@link SwerveMotor} for single line configuration. + */ + public abstract SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder); + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply for position. + */ + public abstract void configureIntegratedEncoder(double positionConversionFactor); + + /** + * Configure the PIDF values for the closed loop controller. 0 is disabled or off. + * + * @param config Configuration class holding the PIDF values. + */ + public abstract void configurePIDF(PIDFConfig config); + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + public abstract void configurePIDWrapping(double minInput, double maxInput); + + /** Disable PID Wrapping on the motor. */ + public abstract void disablePIDWrapping(); + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + public abstract void setMotorBrake(boolean isBrakeMode); + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + public abstract void setInverted(boolean inverted); + + /** Save the configurations from flash to EEPROM. */ + public abstract void burnFlash(); + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + public abstract void set(double percentOutput); + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + public abstract void setReference(double setpoint, double feedforward); + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + public abstract void setReference(double setpoint, double feedforward, double position); + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + public abstract double getVoltage(); + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + public abstract void setVoltage(double voltage); + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + public abstract double getAppliedOutput(); + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity in meters per second or degrees per second. + */ + public abstract double getVelocity(); + + /** + * Get the position of the integrated encoder. + * + * @return Position in meters or degrees. + */ + public abstract double getPosition(); + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. Should be angle in degrees or meters per second. + */ + public abstract void setPosition(double position); + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + public abstract void setVoltageCompensation(double nominalVoltage); + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + public abstract void setCurrentLimit(int currentLimit); + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + public abstract void setLoopRampRate(double rampRate); + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + public abstract Object getMotor(); + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + public abstract DCMotor getSimMotor(); + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + public abstract boolean usingExternalFeedbackSensor(); +} diff --git a/src/main/java/swervelib/motors/TalonFXSSwerve.java b/src/main/java/swervelib/motors/TalonFXSSwerve.java new file mode 100644 index 00000000..248bf0f2 --- /dev/null +++ b/src/main/java/swervelib/motors/TalonFXSSwerve.java @@ -0,0 +1,399 @@ +package swervelib.motors; + +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.Rotations; +import static edu.wpi.first.units.Units.Volts; + +import com.ctre.phoenix6.configs.TalonFXSConfiguration; +import com.ctre.phoenix6.configs.TalonFXSConfigurator; +import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.controls.VelocityVoltage; +import com.ctre.phoenix6.hardware.TalonFXS; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.system.plant.DCMotor; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** {@link TalonFXS} Swerve Motor. Made by Team 1466 WebbRobotics. */ +public class TalonFXSSwerve extends SwerveMotor { + + /** Wait time for status frames to show up. */ + public static double STATUS_TIMEOUT_SECONDS = 0.02; + /** Factory default already occurred. */ + private final boolean factoryDefaultOccurred = false; + /** Whether the absolute encoder is integrated. */ + private final boolean absoluteEncoder = false; + /** Motion magic angle voltage setter. */ + private final MotionMagicVoltage m_angleVoltageSetter = new MotionMagicVoltage(0); + /** Velocity voltage setter for controlling drive motor. */ + private final VelocityVoltage m_velocityVoltageSetter = new VelocityVoltage(0); + /** TalonFXS motor controller. */ + private final TalonFXS motor; + /** Conversion factor for the motor. */ + private double conversionFactor; + /** Current TalonFXS configuration. */ + private TalonFXSConfiguration configuration = new TalonFXSConfiguration(); + /** Current TalonFXS Configurator. */ + private TalonFXSConfigurator cfg; + + /** + * Constructor for TalonFXS swerve motor. + * + * @param motor Motor to use. + * @param isDriveMotor Whether this motor is a drive motor. + * @param motorType {@link DCMotor} which the {@link TalonFXS} is attached to. + */ + public TalonFXSSwerve(TalonFXS motor, boolean isDriveMotor, DCMotor motorType) { + this.isDriveMotor = isDriveMotor; + this.motor = motor; + this.cfg = motor.getConfigurator(); + this.simMotor = motorType; + + factoryDefaults(); + clearStickyFaults(); + } + + /** + * Construct the TalonFXS swerve motor given the ID and CANBus. + * + * @param id ID of the TalonFXS on the CANBus. + * @param canbus CANBus on which the TalonFXS is on. + * @param isDriveMotor Whether the motor is a drive or steering motor. + * @param motorType {@link DCMotor} which the {@link TalonFXS} is attached to. + */ + public TalonFXSSwerve(int id, String canbus, boolean isDriveMotor, DCMotor motorType) { + this(new TalonFXS(id, canbus), isDriveMotor, motorType); + } + + /** + * Construct the TalonFXS swerve motor given the ID. + * + * @param id ID of the TalonFXS on the canbus. + * @param isDriveMotor Whether the motor is a drive or steering motor. + * @param motorType {@link DCMotor} which the {@link TalonFXS} is attached to. + */ + public TalonFXSSwerve(int id, boolean isDriveMotor, DCMotor motorType) { + this(new TalonFXS(id), isDriveMotor, motorType); + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + if (!factoryDefaultOccurred) { + configuration.MotorOutput.NeutralMode = NeutralModeValue.Brake; + configuration.ClosedLoopGeneral.ContinuousWrap = true; + cfg.apply(configuration); + + m_angleVoltageSetter.UpdateFreqHz = 0; + // m_angleVoltageExpoSetter.UpdateFreqHz = 0; + m_velocityVoltageSetter.UpdateFreqHz = 0; + // motor.configFactoryDefault(); + // motor.setSensorPhase(true); + // motor.configSelectedFeedbackSensor(TalonFXSFeedbackDevice.IntegratedSensor, 0, 30); + // motor.configNeutralDeadband(0.001); + } + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + motor.clearStickyFaults(); + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + // Do not support. + return this; + } + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply for position. + *


+ * Degrees:
+ * + * 360 / (angleGearRatio * encoderTicksPerRotation) + *
+ *


+ * Meters:
+ * + * (Math.PI * wheelDiameter) / (driveGearRatio * encoderTicksPerRotation) + * + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + cfg.refresh(configuration); + + positionConversionFactor = 1 / positionConversionFactor; + if (!isDriveMotor) { + positionConversionFactor *= 360; + } + conversionFactor = positionConversionFactor; + + configuration.MotionMagic = + configuration.MotionMagic.withMotionMagicCruiseVelocity(100.0 / positionConversionFactor) + .withMotionMagicAcceleration((100.0 / positionConversionFactor) / 0.100) + .withMotionMagicExpo_kV(0.12 * positionConversionFactor) + .withMotionMagicExpo_kA(0.1); + + /* + configuration.Feedback.withFeedbackSensorSource(FeedbackSensorSourceValue.RotorSensor) + .withSensorToMechanismRatio(positionConversionFactor); + */ + + cfg.apply(configuration); + } + + /** + * Configure the PIDF values for the closed loop controller. 0 is disabled or off. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + + cfg.refresh(configuration.Slot0); + cfg.apply( + configuration.Slot0.withKP(config.p).withKI(config.i).withKD(config.d).withKS(config.f)); + // configuration.slot0.integralZone = config.iz; + // configuration.slot0.closedLoopPeakOutput = config.output.max; + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + cfg.refresh(configuration.ClosedLoopGeneral); + configuration.ClosedLoopGeneral.ContinuousWrap = true; + cfg.apply(configuration.ClosedLoopGeneral); + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + cfg.refresh(configuration.ClosedLoopGeneral); + configuration.ClosedLoopGeneral.ContinuousWrap = false; + cfg.apply(configuration.ClosedLoopGeneral); + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + motor.setNeutralMode(isBrakeMode ? NeutralModeValue.Brake : NeutralModeValue.Coast); + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + // Timer.delay(1); + cfg.refresh(configuration.MotorOutput); + configuration.MotorOutput.withInverted( + inverted ? InvertedValue.CounterClockwise_Positive : InvertedValue.Clockwise_Positive); + cfg.apply(configuration.MotorOutput); + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + // Do nothing + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + setReference(setpoint, feedforward, getPosition()); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + // if (SwerveDriveTelemetry.isSimulation) + // { + // PhysicsSim.getInstance().run(); + // } + + if (isDriveMotor) { + motor.setControl(m_velocityVoltageSetter.withVelocity(setpoint).withFeedForward(feedforward)); + } else { + motor.setControl(m_angleVoltageSetter.withPosition(setpoint / 360.0)); + } + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getMotorVoltage().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue().in(Volts); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + @Override + public double getAppliedOutput() { + return motor.getDutyCycle().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity in Meters Per Second, or Degrees per Second. + */ + @Override + public double getVelocity() { + return motor.getVelocity().getValue().magnitude(); + } + + /** + * Get the position of the integrated encoder. + * + * @return Position in Meters or Degrees. + */ + @Override + public double getPosition() { + return motor.getPosition().getValue().magnitude(); + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. Should be angle in degrees or meters. + */ + @Override + public void setPosition(double position) { + if (!absoluteEncoder && !SwerveDriveTelemetry.isSimulation) { + cfg.setPosition(Degrees.of(position).in(Rotations)); + } + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + // Do not implement + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + cfg.refresh(configuration.CurrentLimits); + cfg.apply( + configuration.CurrentLimits.withSupplyCurrentLimit(currentLimit) + .withSupplyCurrentLimitEnable(true)); + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + cfg.refresh(configuration.ClosedLoopRamps); + cfg.apply(configuration.ClosedLoopRamps.withVoltageClosedLoopRampPeriod(rampRate)); + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + @Override + public DCMotor getSimMotor() { + if (simMotor == null) { + simMotor = DCMotor.getKrakenX60(1); + } + return simMotor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder; + } + + /** Closes handles for unit testing. */ + @Override + public void close() { + motor.close(); + } +} diff --git a/src/main/java/swervelib/motors/TalonFXSwerve.java b/src/main/java/swervelib/motors/TalonFXSwerve.java new file mode 100644 index 00000000..e153c760 --- /dev/null +++ b/src/main/java/swervelib/motors/TalonFXSwerve.java @@ -0,0 +1,402 @@ +package swervelib.motors; + +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.Rotations; +import static edu.wpi.first.units.Units.Volts; + +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfigurator; +import com.ctre.phoenix6.controls.MotionMagicVoltage; +import com.ctre.phoenix6.controls.VelocityVoltage; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.signals.FeedbackSensorSourceValue; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.system.plant.DCMotor; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** {@link com.ctre.phoenix6.hardware.TalonFX} Swerve Motor. Made by Team 1466 WebbRobotics. */ +public class TalonFXSwerve extends SwerveMotor { + + /** Wait time for status frames to show up. */ + public static double STATUS_TIMEOUT_SECONDS = 0.02; + /** Factory default already occurred. */ + private final boolean factoryDefaultOccurred = false; + /** Whether the absolute encoder is integrated. */ + private final boolean absoluteEncoder = false; + /** Motion magic angle voltage setter. */ + private final MotionMagicVoltage m_angleVoltageSetter = new MotionMagicVoltage(0); + /** Velocity voltage setter for controlling drive motor. */ + private final VelocityVoltage m_velocityVoltageSetter = new VelocityVoltage(0); + /** TalonFX motor controller. */ + private final TalonFX motor; + /** Conversion factor for the motor. */ + private double conversionFactor; + /** Current TalonFX configuration. */ + private TalonFXConfiguration configuration = new TalonFXConfiguration(); + /** Current TalonFX Configurator. */ + private TalonFXConfigurator cfg; + + /** + * Constructor for TalonFX swerve motor. + * + * @param motor Motor to use. + * @param isDriveMotor Whether this motor is a drive motor. + * @param motorType {@link DCMotor} which the {@link TalonFX} is attached to. + */ + public TalonFXSwerve(TalonFX motor, boolean isDriveMotor, DCMotor motorType) { + this.isDriveMotor = isDriveMotor; + this.motor = motor; + this.cfg = motor.getConfigurator(); + this.simMotor = motorType; + + factoryDefaults(); + clearStickyFaults(); + + // if (SwerveDriveTelemetry.isSimulation) + // { + //// PhysicsSim.getInstance().addTalonFX(motor, .25, 6800); + // } + } + + /** + * Construct the TalonFX swerve motor given the ID and CANBus. + * + * @param id ID of the TalonFX on the CANBus. + * @param canbus CANBus on which the TalonFX is on. + * @param isDriveMotor Whether the motor is a drive or steering motor. + * @param motorType {@link DCMotor} which the {@link TalonFX} is attached to. + */ + public TalonFXSwerve(int id, String canbus, boolean isDriveMotor, DCMotor motorType) { + this(new TalonFX(id, canbus), isDriveMotor, motorType); + } + + /** + * Construct the TalonFX swerve motor given the ID. + * + * @param id ID of the TalonFX on the canbus. + * @param isDriveMotor Whether the motor is a drive or steering motor. + * @param motorType {@link DCMotor} which the {@link TalonFX} is attached to. + */ + public TalonFXSwerve(int id, boolean isDriveMotor, DCMotor motorType) { + this(new TalonFX(id), isDriveMotor, motorType); + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + if (!factoryDefaultOccurred) { + configuration.MotorOutput.NeutralMode = NeutralModeValue.Brake; + configuration.ClosedLoopGeneral.ContinuousWrap = true; + cfg.apply(configuration); + + m_angleVoltageSetter.UpdateFreqHz = 0; + // m_angleVoltageExpoSetter.UpdateFreqHz = 0; + m_velocityVoltageSetter.UpdateFreqHz = 0; + // motor.configFactoryDefault(); + // motor.setSensorPhase(true); + // motor.configSelectedFeedbackSensor(TalonFXFeedbackDevice.IntegratedSensor, 0, 30); + // motor.configNeutralDeadband(0.001); + } + } + + @Override + public void close() { + motor.close(); + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + motor.clearStickyFaults(); + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + // Do not support. + return this; + } + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply for position. + *


+ * Degrees:
+ * + * 360 / (angleGearRatio * encoderTicksPerRotation) + *
+ *


+ * Meters:
+ * + * (Math.PI * wheelDiameter) / (driveGearRatio * encoderTicksPerRotation) + * + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + cfg.refresh(configuration); + + positionConversionFactor = 1 / positionConversionFactor; + if (!isDriveMotor) { + positionConversionFactor *= 360; + } + conversionFactor = positionConversionFactor; + + configuration.MotionMagic = + configuration.MotionMagic.withMotionMagicCruiseVelocity(100.0 / positionConversionFactor) + .withMotionMagicAcceleration((100.0 / positionConversionFactor) / 0.100) + .withMotionMagicExpo_kV(0.12 * positionConversionFactor) + .withMotionMagicExpo_kA(0.1); + + configuration.Feedback.withFeedbackSensorSource(FeedbackSensorSourceValue.RotorSensor) + .withSensorToMechanismRatio(positionConversionFactor); + + cfg.apply(configuration); + } + + /** + * Configure the PIDF values for the closed loop controller. 0 is disabled or off. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + + cfg.refresh(configuration.Slot0); + cfg.apply( + configuration.Slot0.withKP(config.p).withKI(config.i).withKD(config.d).withKS(config.f)); + // configuration.slot0.integralZone = config.iz; + // configuration.slot0.closedLoopPeakOutput = config.output.max; + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + cfg.refresh(configuration.ClosedLoopGeneral); + configuration.ClosedLoopGeneral.ContinuousWrap = true; + cfg.apply(configuration.ClosedLoopGeneral); + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + cfg.refresh(configuration.ClosedLoopGeneral); + configuration.ClosedLoopGeneral.ContinuousWrap = false; + cfg.apply(configuration.ClosedLoopGeneral); + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + motor.setNeutralMode(isBrakeMode ? NeutralModeValue.Brake : NeutralModeValue.Coast); + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + // Timer.delay(1); + cfg.refresh(configuration.MotorOutput); + configuration.MotorOutput.withInverted( + inverted ? InvertedValue.CounterClockwise_Positive : InvertedValue.Clockwise_Positive); + cfg.apply(configuration.MotorOutput); + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + // Do nothing + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + setReference(setpoint, feedforward, getPosition()); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + // if (SwerveDriveTelemetry.isSimulation) + // { + // PhysicsSim.getInstance().run(); + // } + + if (isDriveMotor) { + motor.setControl(m_velocityVoltageSetter.withVelocity(setpoint).withFeedForward(feedforward)); + } else { + motor.setControl(m_angleVoltageSetter.withPosition(setpoint / 360.0)); + } + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getMotorVoltage().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue().in(Volts); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + @Override + public double getAppliedOutput() { + return motor.getDutyCycle().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity in Meters Per Second, or Degrees per Second. + */ + @Override + public double getVelocity() { + return motor.getVelocity().getValue().magnitude(); + } + + /** + * Get the position of the integrated encoder. + * + * @return Position in Meters or Degrees. + */ + @Override + public double getPosition() { + return motor.getPosition().getValue().magnitude(); + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. Should be angle in degrees or meters. + */ + @Override + public void setPosition(double position) { + if (!absoluteEncoder && !SwerveDriveTelemetry.isSimulation) { + cfg.setPosition(Degrees.of(position).in(Rotations)); + } + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + // Do not implement + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + cfg.refresh(configuration.CurrentLimits); + cfg.apply( + configuration.CurrentLimits.withSupplyCurrentLimit(currentLimit) + .withSupplyCurrentLimitEnable(true)); + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + cfg.refresh(configuration.ClosedLoopRamps); + cfg.apply(configuration.ClosedLoopRamps.withVoltageClosedLoopRampPeriod(rampRate)); + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + @Override + public DCMotor getSimMotor() { + if (simMotor == null) { + simMotor = DCMotor.getKrakenX60(1); + } + return simMotor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder; + } +} diff --git a/src/main/java/swervelib/motors/TalonSRXSwerve.java b/src/main/java/swervelib/motors/TalonSRXSwerve.java new file mode 100644 index 00000000..92fac1f7 --- /dev/null +++ b/src/main/java/swervelib/motors/TalonSRXSwerve.java @@ -0,0 +1,440 @@ +package swervelib.motors; + +import com.ctre.phoenix.motorcontrol.ControlMode; +import com.ctre.phoenix.motorcontrol.DemandType; +import com.ctre.phoenix.motorcontrol.FeedbackDevice; +import com.ctre.phoenix.motorcontrol.NeutralMode; +import com.ctre.phoenix.motorcontrol.StatusFrameEnhanced; +import com.ctre.phoenix.motorcontrol.can.TalonSRXConfiguration; +import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX; +import edu.wpi.first.math.system.plant.DCMotor; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.encoders.TalonSRXEncoderSwerve; +import swervelib.math.SwerveMath; +import swervelib.parser.PIDFConfig; +import swervelib.parser.json.modules.ConversionFactorsJson; +import swervelib.telemetry.SwerveDriveTelemetry; + +/** {@link com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX} Swerve Motor. */ +public class TalonSRXSwerve extends SwerveMotor { + + /** Factory default already occurred. */ + private final boolean factoryDefaultOccurred = false; + /** Current TalonSRX configuration. */ + private final TalonSRXConfiguration configuration = new TalonSRXConfiguration(); + /** Whether the absolute encoder is integrated. */ + private boolean absoluteEncoder = false; + /** TalonSRX motor controller. */ + private final WPI_TalonSRX motor; + /** + * The position conversion factor to convert raw sensor units to Meters Per 100ms, or Ticks to + * Degrees. + */ + private double positionConversionFactor = 1; + /** Module Conversion factors to use. */ + private ConversionFactorsJson moduleConversionFactors; + /** If the TalonSRX configuration has changed. */ + private boolean configChanged = true; + /** Nominal voltage default to use with feedforward. */ + private double nominalVoltage = 12.0; + + /** + * Constructor for TalonSRX swerve motor. + * + * @param motor Motor to use. + * @param isDriveMotor Whether this motor is a drive motor. + * @param motorType {@link DCMotor} which the {@link WPI_TalonSRX} is attached to. + */ + public TalonSRXSwerve(WPI_TalonSRX motor, boolean isDriveMotor, DCMotor motorType) { + this.isDriveMotor = isDriveMotor; + this.motor = motor; + this.simMotor = motorType; + motor.configSelectedFeedbackSensor(FeedbackDevice.QuadEncoder); + + factoryDefaults(); + clearStickyFaults(); + } + + /** + * Construct the TalonSRX swerve motor given the ID. + * + * @param id ID of the TalonSRX on the canbus. + * @param isDriveMotor Whether the motor is a drive or steering motor. + * @param motorType {@link DCMotor} which the {@link WPI_TalonSRX} is attached to. + */ + public TalonSRXSwerve(int id, boolean isDriveMotor, DCMotor motorType) { + this(new WPI_TalonSRX(id), isDriveMotor, motorType); + } + + @Override + public void close() { + motor.close(); + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + if (!factoryDefaultOccurred) { + motor.configFactoryDefault(); + motor.setSensorPhase(true); + } + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + motor.clearStickyFaults(); + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + absoluteEncoder = encoder instanceof TalonSRXEncoderSwerve; + // Do not support. + return this; + } + + /** + * Configure the integrated encoder for the swerve module. Sets the conversion factors for + * position and velocity. + * + * @param positionConversionFactor The conversion factor to apply for position. + *


+ * Degrees:
+ * + * 360 / (angleGearRatio * encoderTicksPerRotation) + *
+ *


+ * Meters:
+ * + * (Math.PI * wheelDiameter) / (driveGearRatio * encoderTicksPerRotation) + * + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + this.positionConversionFactor = positionConversionFactor; + // Taken from democat's library. + // https://github.com/democat3457/swerve-lib/blob/7c03126b8c22f23a501b2c2742f9d173a5bcbc40/src/main/java/com/swervedrivespecialties/swervelib/ctre/Falcon500DriveControllerFactoryBuilder.java#L16 + configureCANStatusFrames(250); + } + + /** + * Set the CAN status frames. + * + * @param CANStatus1 Applied Motor Output, Fault Information, Limit Switch Information + */ + public void configureCANStatusFrames(int CANStatus1) { + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_1_General, CANStatus1); + } + + /** + * Set the CAN status frames. + * + * @param CANStatus1 Applied Motor Output, Fault Information, Limit Switch Information + * @param CANStatus2 Selected Sensor Position (PID 0), Selected Sensor Velocity (PID 0), Brushed + * Supply Current Measurement, Sticky Fault Information + * @param CANStatus3 Quadrature Information + * @param CANStatus4 Analog Input, Supply Battery Voltage, Controller Temperature + * @param CANStatus8 Pulse Width Information + * @param CANStatus10 Motion Profiling/Motion Magic Information + * @param CANStatus12 Selected Sensor Position (Aux PID 1), Selected Sensor Velocity (Aux PID 1) + * @param CANStatus13 PID0 (Primary PID) Information + * @param CANStatus14 PID1 (Auxiliary PID) Information + * @param CANStatus21 Integrated Sensor Position (Talon FX), Integrated Sensor Velocity (Talon FX) + * @param CANStatusCurrent Brushless Supply Current Measurement, Brushless Stator Current + * Measurement + */ + public void configureCANStatusFrames( + int CANStatus1, + int CANStatus2, + int CANStatus3, + int CANStatus4, + int CANStatus8, + int CANStatus10, + int CANStatus12, + int CANStatus13, + int CANStatus14, + int CANStatus21, + int CANStatusCurrent) { + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_1_General, CANStatus1); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_2_Feedback0, CANStatus2); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_3_Quadrature, CANStatus3); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_4_AinTempVbat, CANStatus4); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_8_PulseWidth, CANStatus8); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_10_Targets, CANStatus10); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_12_Feedback1, CANStatus12); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_13_Base_PIDF0, CANStatus13); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_14_Turn_PIDF1, CANStatus14); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_21_FeedbackIntegrated, CANStatus21); + motor.setStatusFramePeriod(StatusFrameEnhanced.Status_Brushless_Current, CANStatusCurrent); + + // TODO: Configure Status Frame 2 thru 21 if necessary + // https://v5.docs.ctr-electronics.com/en/stable/ch18_CommonAPI.html#setting-status-frame-periods + } + + /** + * Configure the PIDF values for the closed loop controller. 0 is disabled or off. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + configuration.slot0.kP = config.p; + configuration.slot0.kI = config.i; + configuration.slot0.kD = config.d; + configuration.slot0.kF = config.f; + configuration.slot0.integralZone = config.iz; + configuration.slot0.closedLoopPeakOutput = config.output.max; + configChanged = true; + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + // Do nothing + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + // Do nothing + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + motor.setNeutralMode(isBrakeMode ? NeutralMode.Brake : NeutralMode.Coast); + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + motor.setInverted(inverted); + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + if (configChanged) { + motor.configAllSettings(configuration, 250); + configChanged = false; + } + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Convert the setpoint into native sensor units. + * + * @param setpoint Setpoint to mutate. In meters per second or degrees. + * @param position Position in degrees, only used on angle motors. + * @return Setpoint as native sensor units. Encoder ticks per 100ms, or Encoder tick. + */ + public double convertToNativeSensorUnits(double setpoint, double position) { + setpoint = + isDriveMotor ? setpoint * .1 : SwerveMath.placeInAppropriate0To360Scope(position, setpoint); + return setpoint / positionConversionFactor; + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + setReference(setpoint, feedforward, getPosition()); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + + burnFlash(); + + motor.set( + isDriveMotor ? ControlMode.Velocity : ControlMode.Position, + convertToNativeSensorUnits(setpoint, position), + DemandType.ArbitraryFeedForward, + feedforward / nominalVoltage); + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getMotorOutputVoltage(); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the applied dutycycle output. + * + * @return Applied dutycycle output to the motor. + */ + @Override + public double getAppliedOutput() { + return motor.getMotorOutputPercent(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity in Meters Per Second, or Degrees per Second. + */ + @Override + public double getVelocity() { + return (motor.getSelectedSensorVelocity() * 10) * positionConversionFactor; + } + + /** + * Get the position of the integrated encoder. + * + * @return Position in Meters or Degrees. + */ + @Override + public double getPosition() { + if (isDriveMotor) { + return motor.getSelectedSensorPosition() * positionConversionFactor; + } else { + var pos = motor.getSelectedSensorPosition() * positionConversionFactor; + return pos; + } + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. Should be angle in degrees or meters. + */ + @Override + public void setPosition(double position) { + if (!absoluteEncoder && !SwerveDriveTelemetry.isSimulation) { + motor.setSelectedSensorPosition(position / positionConversionFactor, 0, 0); + } + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + configuration.voltageCompSaturation = nominalVoltage; + configChanged = true; + this.nominalVoltage = nominalVoltage; + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + configuration.continuousCurrentLimit = currentLimit; + configuration.peakCurrentLimit = currentLimit; + configChanged = true; + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + configuration.closedloopRamp = rampRate; + configuration.openloopRamp = rampRate; + configChanged = true; + } + + /** + * Set the selected feedback device for the TalonSRX. + * + * @param feedbackDevice Feedback device to select. + */ + public void setSelectedFeedbackDevice(FeedbackDevice feedbackDevice) { + configuration.primaryPID.selectedFeedbackSensor = feedbackDevice; + configChanged = true; + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Get the {@link DCMotor} of the motor class. + * + * @return {@link DCMotor} of this type. + */ + @Override + public DCMotor getSimMotor() { + if (simMotor == null) { + simMotor = DCMotor.getCIM(1); + } + return simMotor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder; + } +} diff --git a/src/main/java/swervelib/motors/ThriftyNovaSwerve.java b/src/main/java/swervelib/motors/ThriftyNovaSwerve.java new file mode 100644 index 00000000..61237ad2 --- /dev/null +++ b/src/main/java/swervelib/motors/ThriftyNovaSwerve.java @@ -0,0 +1,471 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package swervelib.motors; + +import com.thethriftybot.Conversion; +import com.thethriftybot.Conversion.PositionUnit; +import com.thethriftybot.Conversion.VelocityUnit; +import com.thethriftybot.ThriftyNova; +import com.thethriftybot.ThriftyNova.CurrentType; +import com.thethriftybot.ThriftyNova.EncoderType; +import com.thethriftybot.ThriftyNova.PIDSlot; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.DataLogManager; +import edu.wpi.first.wpilibj.RobotBase; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.parser.PIDFConfig; +import swervelib.telemetry.SwerveDriveTelemetry; +import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; + +/** An implementation of {@link ThriftyNova} as a {@link SwerveMotor}. */ +public class ThriftyNovaSwerve extends SwerveMotor { + + /** {@link DCMotor} for simulation and calculations. */ + private final DCMotor simMotor; + /** Closed-loop PID controller. */ + public PIDController pid; + /** ThriftyNova Instance. */ + private ThriftyNova motor; + /** Factory default already occurred. */ + private boolean factoryDefaultOccurred = false; + /** Position conversion object for the motor encoder */ + private Conversion positionConversion; + /** Velocity conversion object for the motor encoder */ + private Conversion velocityConversion; + /** The position conversion factor for the encoder */ + private double positionConversionFactor = 1.0; + /** The position conversion factor for the encoder */ + private double velocityConversionFactor = 1.0 / 60.0; + /** Absolute encoder attached to the SparkMax (if exists) */ + private Optional absoluteEncoder = Optional.empty(); + /** Supplier for the velocity of the motor controller. */ + private Supplier velocity; + /** Supplier for the position of the motor controller. */ + private Supplier position; + + /** + * Initialize the swerve motor. + * + * @param motor The SwerveMotor as a ThriftyNova object. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param motorType {@link DCMotor} controlled by the {@link ThriftyNova} + */ + public ThriftyNovaSwerve(ThriftyNova motor, boolean isDriveMotor, DCMotor motorType) { + this.motor = motor; + this.isDriveMotor = isDriveMotor; + this.simMotor = motorType; + factoryDefaults(); + clearStickyFaults(); + + motor.usePIDSlot(PIDSlot.SLOT0); + pid = new PIDController(0, 0, 0); + motor.pid0.setPID(pid); + + if (isDriveMotor) { + positionConversion = new Conversion(PositionUnit.ROTATIONS, EncoderType.INTERNAL); + velocityConversion = new Conversion(VelocityUnit.ROTATIONS_PER_SEC, EncoderType.INTERNAL); + } else { + positionConversion = new Conversion(PositionUnit.ROTATIONS, EncoderType.INTERNAL); + velocityConversion = new Conversion(VelocityUnit.ROTATIONS_PER_SEC, EncoderType.INTERNAL); + } + + position = this::getConvertedPosition; + velocity = this::getConvertedVelocity; + } + + /** + * Initialize the {@link SwerveMotor} as a {@link ThriftyNova} connected to a Brushless Motor. + * + * @param id CAN ID of the ThriftyNova. + * @param isDriveMotor Is the motor being initialized a drive motor? + * @param motor {@link DCMotor} controlled by the {@link ThriftyNova} + */ + public ThriftyNovaSwerve(int id, boolean isDriveMotor, DCMotor motor) { + this(new ThriftyNova(id), isDriveMotor, motor); + } + + /** + * Close the motor controller and release any resources it may have acquired. + * + * @throws RuntimeException If an exception is thrown while closing the motor. + */ + @Override + public void close() { + try { + motor.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private double getConvertedPosition() { + double motorPosition = motor.getPosition(); + double convertedPosition = motorPosition / positionConversionFactor; + return convertedPosition; + } + + /** + * Get the velocity of the motor in meters per second or degrees per second after conversion. + * + * @return velocity in meters per second or degrees per second. + */ + private double getConvertedVelocity() { + double motorVelocity = motor.getVelocity(); + double convertedVelocity = motorVelocity / velocityConversionFactor; + + return convertedVelocity; + } + + /** Set factory defaults on the motor controller. */ + @Override + public void factoryDefaults() { + // Factory defaults from + // https://docs.thethriftybot.com/thrifty-nova/gqCPUYXcVoOZ4KW3DqIr/software-resources/configure-controller-settings/factory-default + if (!factoryDefaultOccurred) { + if (RobotBase.isReal()) { + motor.setInverted(false); + motor.setBrakeMode(false); + setCurrentLimit(40); + motor.setEncoderPosition(0); + motor.setMaxOutput(1.0); + motor.setRampDown(100); + motor.setRampUp(100); + configureCANStatusFrames(0.25, 0.1, 0.25, 0.5, 0.50); + motor.setSoftLimits(0, 0); + configurePIDF(new PIDFConfig()); + motor.pid1.setP(0).setI(0).setD(0).setFF(0.0); + } + factoryDefaultOccurred = true; + } + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + if (RobotBase.isReal()) { + motor.clearErrors(); + } + } + + /** + * Set the absolute encoder to be a compatible absolute encoder. + * + * @param encoder The encoder to use. + * @return The {@link SwerveMotor} for easy instantiation. + */ + @Override + public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { + if (encoder == null) { + motor.useEncoderType(EncoderType.INTERNAL); + position = this::getConvertedPosition; + velocity = this::getConvertedVelocity; + } else { + absoluteEncoder = Optional.of(encoder); + position = absoluteEncoder.get()::getAbsolutePosition; + velocity = absoluteEncoder.get()::getVelocity; + motor.useEncoderType(EncoderType.ABS); + } + return this; + } + + /** + * Configure the integrated encoder for the ThriftyNova swerve module. Sets the conversion factors + * for position and velocity, and configures the CAN status frames. + * + * @param positionConversionFactor The conversion factor to apply for position. + */ + @Override + public void configureIntegratedEncoder(double positionConversionFactor) { + this.positionConversionFactor = positionConversionFactor; + this.velocityConversionFactor = positionConversionFactor; + + motor.useEncoderType(EncoderType.INTERNAL); + configureCANStatusFrames(0.25, 0.01, 0.01, 0.02, 0.20); + } + + /** + * Set the CAN status frames. + * + * @param fault Fault transmission rate + * @param sensor Sensor transmission rate + * @param quadSensor External quad encoder transmission rate + * @param control Control frame transmission rate + * @param current Current feedback transmission rate + */ + public void configureCANStatusFrames( + double fault, double sensor, double quadSensor, double control, double current) { + if (RobotBase.isReal()) { + motor.canFreq.setFault(fault); + motor.canFreq.setSensor(sensor); + motor.canFreq.setQuadSensor(quadSensor); + motor.canFreq.setControl(control); + motor.canFreq.setCurrent(current); + checkErrors("Configuring CAN status frames failed: "); + } + } + + /** + * Configure the PIDF values for the closed loop controller. 0 is disabled or off. + * + * @param config Configuration class holding the PIDF values. + */ + @Override + public void configurePIDF(PIDFConfig config) { + if (RobotBase.isReal()) { + motor.pid0.setP(config.p).setI(config.i).setD(config.d); + motor.usePIDSlot(PIDSlot.SLOT0); + checkErrors("Configuring PIDF failed: "); + } + } + + /** + * Configure the PID wrapping for the position closed loop controller. + * + * @param minInput Minimum PID input. + * @param maxInput Maximum PID input. + */ + @Override + public void configurePIDWrapping(double minInput, double maxInput) { + // Do nothing + } + + /** Disable PID Wrapping on the motor. */ + @Override + public void disablePIDWrapping() { + // Do nothing + } + + /** + * Set the idle mode. + * + * @param isBrakeMode Set the brake mode. + */ + @Override + public void setMotorBrake(boolean isBrakeMode) { + if (RobotBase.isReal()) { + motor.setBrakeMode(isBrakeMode); + checkErrors("Setting motor brake mode failed: "); + } + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + if (RobotBase.isReal()) { + motor.setInverted(inverted); + checkErrors("Setting motor inversion failed: "); + } + } + + /** Save the configurations from flash to EEPROM. */ + @Override + public void burnFlash() { + // Do nothing + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.setPercent(percentOutput); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in MPS or Angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + */ + @Override + public void setReference(double setpoint, double feedforward) { + setReference(setpoint, feedforward, getPosition()); + } + + /** + * Set the closed loop PID controller reference point. + * + * @param setpoint Setpoint in meters per second or angle in degrees. + * @param feedforward Feedforward in volt-meter-per-second or kV. + * @param position Only used on the angle motor, the position of the motor in degrees. + */ + @Override + public void setReference(double setpoint, double feedforward, double position) { + if (RobotBase.isReal()) { + if (isDriveMotor) { + double convertedSetpoint = setpoint / velocityConversionFactor; + double motorSetpoint = velocityConversion.toMotor(convertedSetpoint); + motor.setVelocity(motorSetpoint, feedforward); + } else { + double convertedSetpoint = + absoluteEncoder.map(it -> setpoint).orElse(setpoint / positionConversionFactor); + double motorSetpoint = positionConversion.toMotor(convertedSetpoint); + motor.setPosition(motorSetpoint); + } + } + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getVoltage() { + return motor.getVoltage(); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the voltage output of the motor controller. + * + * @return Voltage output. + */ + @Override + public double getAppliedOutput() { + return motor.getStatorCurrent(); + } + + /** + * Get the velocity of the integrated encoder. + * + * @return velocity in Meters Per Second, or Degrees per Second. + */ + @Override + public double getVelocity() { + return velocity.get(); + } + + /** + * Get the position of the integrated encoder. + * + * @return Position in Meters or Degrees. + */ + @Override + public double getPosition() { + return position.get(); + } + + /** + * Set the integrated encoder position. + * + * @param position Integrated encoder position. Should be angle in degrees or meters. + */ + @Override + public void setPosition(double position) { + if (!absoluteEncoder.isPresent()) { + double convertedPosition = position / positionConversionFactor; + double motorPosition = positionConversion.toMotor(convertedPosition); + motor.setEncoderPosition(motorPosition); + } + } + + /** + * Set the voltage compensation for the swerve module motor. + * + * @param nominalVoltage Nominal voltage for operation to output to. + */ + @Override + public void setVoltageCompensation(double nominalVoltage) { + motor.setVoltageCompensation(nominalVoltage); + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param currentLimit Current limit in AMPS at free speed. + */ + @Override + public void setCurrentLimit(int currentLimit) { + if (RobotBase.isReal()) { + motor.setMaxCurrent(CurrentType.STATOR, currentLimit); + checkErrors("Setting current limit failed: "); + } + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rampRate Time in seconds to go from 0 to full throttle. + */ + @Override + public void setLoopRampRate(double rampRate) { + if (RobotBase.isReal()) { + motor.setRampUp(rampRate); + motor.setRampDown(rampRate); + checkErrors("Setting loop ramp rate failed: "); + } + } + + /** + * Get the motor object from the module. + * + * @return Motor object. + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Queries whether the absolute encoder is directly attached to the motor controller. + * + * @return connected absolute encoder state. + */ + @Override + public boolean usingExternalFeedbackSensor() { + return absoluteEncoder.isPresent(); + } + + /** + * Checks for errors in the motor and logs them if any are found. + * + * @param message the message to prepend to the log and print statement + */ + private void checkErrors(String message) { + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.HIGH.ordinal()) { + List errors = motor.getErrors(); + if (errors.size() > 0) { + for (ThriftyNova.Error error : errors) { + if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.MACHINE.ordinal()) { + System.out.println(this.getClass().getSimpleName() + ": " + message + error.toString()); + } + DataLogManager.log(this.getClass().getSimpleName() + ": " + message + error.toString()); + } + } + motor.clearErrors(); + } + } + + /** + * Get the simulated {@link DCMotor} associated with this motor controller. + * + * @return Simulated {@link DCMotor} instance. + */ + @Override + public DCMotor getSimMotor() { + return simMotor; + } +} diff --git a/src/main/java/swervelib/motors/package-info.java b/src/main/java/swervelib/motors/package-info.java new file mode 100644 index 00000000..de65fb08 --- /dev/null +++ b/src/main/java/swervelib/motors/package-info.java @@ -0,0 +1,2 @@ +/** Swerve motor controller wrappers which implement {@link swervelib.motors.SwerveMotor}. */ +package swervelib.motors; diff --git a/src/main/java/swervelib/package-info.java b/src/main/java/swervelib/package-info.java new file mode 100644 index 00000000..a9884ca3 --- /dev/null +++ b/src/main/java/swervelib/package-info.java @@ -0,0 +1,6 @@ +/** + * Yet-Another Generic Swerve Library (YAGSL) main package AKA swervelib. + * + * @version 1.0.0 + */ +package swervelib; diff --git a/src/main/java/swervelib/parser/Cache.java b/src/main/java/swervelib/parser/Cache.java new file mode 100644 index 00000000..debc91cb --- /dev/null +++ b/src/main/java/swervelib/parser/Cache.java @@ -0,0 +1,87 @@ +package swervelib.parser; + +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj.RobotController; +import java.util.function.Supplier; + +/** Cache for frequently requested data. */ +public class Cache { + + /** Cached value. */ + private T value; + /** Supplier for cached value. */ + private Supplier supplier; + /** Timestamp in microseconds. */ + private long timestamp; + /** Validity period in microseconds. */ + private long validityPeriod; + + /** + * Cache for arbitrary values. + * + * @param val Value to cache. + * @param validityPeriod Validity period in milliseconds. + */ + public Cache(Supplier val, long validityPeriod) { + supplier = val; + value = supplier.get(); + timestamp = RobotController.getFPGATime(); + this.validityPeriod = validityPeriod * 1000L; + } + + /** + * Return whether the cache is stale. + * + * @return The stale state of the cache. + */ + public boolean isStale() { + return (RobotController.getFPGATime() - timestamp) > validityPeriod; + } + + /** + * Update the cache value and timestamp. + * + * @return {@link Cache} used. + */ + public Cache update() { + this.value = supplier.get(); + this.timestamp = RobotController.getFPGATime(); + return this; + } + + /** + * Update the supplier to a new source. Updates the value and timestamp as well. + * + * @param supplier new supplier source. + * @return {@link Cache} for chaining. + */ + public Cache updateSupplier(Supplier supplier) { + this.supplier = supplier; + update(); + return this; + } + + /** + * Update the validity period for the cached value, also updates the value. + * + * @param validityPeriod The new validity period in milliseconds. + * @return {@link Cache} for chaining. + */ + public Cache updateValidityPeriod(long validityPeriod) { + this.validityPeriod = validityPeriod * 1000L; + update(); + return this; + } + + /** + * Get the most up to date cached value. + * + * @return {@link T} updated to the latest cached version. + */ + public T getValue() { + if (isStale() || RobotBase.isSimulation()) { + update(); + } + return value; + } +} diff --git a/src/main/java/swervelib/parser/PIDFConfig.java b/src/main/java/swervelib/parser/PIDFConfig.java new file mode 100644 index 00000000..5e3f25c6 --- /dev/null +++ b/src/main/java/swervelib/parser/PIDFConfig.java @@ -0,0 +1,88 @@ +package swervelib.parser; + +import edu.wpi.first.math.controller.PIDController; +import swervelib.parser.deserializer.PIDFRange; + +/** Hold the PIDF and Integral Zone values for a PID. */ +public class PIDFConfig { + + /** Proportional Gain for PID. */ + public double p; + /** Integral Gain for PID. */ + public double i; + /** Derivative Gain for PID. */ + public double d; + /** Feedforward value for PID. */ + public double f; + /** Integral zone of the PID. */ + public double iz; + + /** The PIDF output range. */ + public PIDFRange output = new PIDFRange(); + + /** Used when parsing PIDF values from JSON. */ + public PIDFConfig() {} + + /** + * PIDF Config constructor to contain the values. + * + * @param p P gain. + * @param i I gain. + * @param d D gain. + * @param f F gain. + * @param iz Intergral zone. + */ + public PIDFConfig(double p, double i, double d, double f, double iz) { + this.p = p; + this.i = i; + this.d = d; + this.f = f; + this.iz = iz; + } + + /** + * PIDF Config constructor to contain the values. + * + * @param p P gain. + * @param i I gain. + * @param d D gain. + * @param f F gain. + */ + public PIDFConfig(double p, double i, double d, double f) { + this(p, i, d, f, 0); + } + + /** + * PIDF Config constructor to contain the values. + * + * @param p P gain. + * @param i I gain. + * @param d D gain. + */ + public PIDFConfig(double p, double i, double d) { + this(p, i, d, 0, 0); + } + + /** + * PIDF Config constructor to contain the values. + * + * @param p P gain. + * @param d D gain. + */ + public PIDFConfig(double p, double d) { + this(p, 0, d, 0, 0); + } + + /** + * Create a PIDController from the PID values. + * + * @return PIDController. + */ + public PIDController createPIDController() { + PIDController pidController = new PIDController(p, i, d); + if (iz != 0) { + pidController.setIZone(iz); + } + return pidController; + } +} diff --git a/src/main/java/swervelib/parser/SwerveControllerConfiguration.java b/src/main/java/swervelib/parser/SwerveControllerConfiguration.java new file mode 100644 index 00000000..7961032f --- /dev/null +++ b/src/main/java/swervelib/parser/SwerveControllerConfiguration.java @@ -0,0 +1,57 @@ +package swervelib.parser; + +import static swervelib.math.SwerveMath.calculateMaxAngularVelocity; + +/** + * Swerve Controller configuration class which is used to configure {@link + * swervelib.SwerveController}. + */ +public class SwerveControllerConfiguration { + + /** PIDF for the heading of the robot. */ + public final PIDFConfig headingPIDF; + /** hypotenuse deadband for the robot angle control joystick. */ + public final double + angleJoyStickRadiusDeadband; // Deadband for the minimum hypot for the heading joystick. + /** Maximum chassis angular velocity in rad/s */ + public double maxAngularVelocity; + + /** + * Construct the swerve controller configuration. Assumes robot is square to fetch maximum angular + * velocity. + * + * @param driveCfg {@link SwerveDriveConfiguration} to fetch the first module X and Y used to + * calculate the maximum angular velocity. + * @param headingPIDF Heading PIDF configuration. + * @param angleJoyStickRadiusDeadband Deadband on radius of angle joystick. + * @param maxSpeedMPS Maximum speed in meters per second for angular velocity, remember if you + * have feet per second use {@link edu.wpi.first.math.util.Units#feetToMeters(double)}. + */ + public SwerveControllerConfiguration( + SwerveDriveConfiguration driveCfg, + PIDFConfig headingPIDF, + double angleJoyStickRadiusDeadband, + double maxSpeedMPS) { + this.maxAngularVelocity = + calculateMaxAngularVelocity( + maxSpeedMPS, + Math.abs(driveCfg.moduleLocationsMeters[0].getX()), + Math.abs(driveCfg.moduleLocationsMeters[0].getY())); + this.headingPIDF = headingPIDF; + this.angleJoyStickRadiusDeadband = angleJoyStickRadiusDeadband; + } + + /** + * Construct the swerve controller configuration. Assumes hypotenuse deadband of 0.5 (minimum + * radius for angle to be set on angle joystick is .5 of the controller). + * + * @param driveCfg Drive configuration. + * @param headingPIDF Heading PIDF configuration. + * @param maxSpeedMPS Maximum speed in meters per second for angular velocity, remember if you + * have feet per second use {@link edu.wpi.first.math.util.Units#feetToMeters(double)}. + */ + public SwerveControllerConfiguration( + SwerveDriveConfiguration driveCfg, PIDFConfig headingPIDF, double maxSpeedMPS) { + this(driveCfg, headingPIDF, 0.5, maxSpeedMPS); + } +} diff --git a/src/main/java/swervelib/parser/SwerveDriveConfiguration.java b/src/main/java/swervelib/parser/SwerveDriveConfiguration.java new file mode 100644 index 00000000..433f7252 --- /dev/null +++ b/src/main/java/swervelib/parser/SwerveDriveConfiguration.java @@ -0,0 +1,139 @@ +package swervelib.parser; + +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.system.plant.DCMotor; +import java.util.function.Supplier; +import org.ironmaple.simulation.drivesims.COTS; +import org.ironmaple.simulation.drivesims.GyroSimulation; +import swervelib.SwerveModule; +import swervelib.imu.NavXSwerve; +import swervelib.imu.Pigeon2Swerve; +import swervelib.imu.SwerveIMU; +import swervelib.math.SwerveMath; + +/** Swerve drive configurations used during SwerveDrive construction. */ +public class SwerveDriveConfiguration { + + /** Number of modules on the robot. */ + public final int moduleCount; + /** Swerve Module locations. */ + public Translation2d[] moduleLocationsMeters; + /** Swerve IMU */ + public SwerveIMU imu; + /** Swerve Modules. */ + public SwerveModule[] modules; + /** Physical characteristics of the swerve drive from physicalproperties.json. */ + public SwerveModulePhysicalCharacteristics physicalCharacteristics; + + /** + * Create swerve drive configuration. + * + * @param moduleConfigs Module configuration. + * @param swerveIMU Swerve IMU. + * @param invertedIMU Invert the IMU. + * @param physicalCharacteristics {@link SwerveModulePhysicalCharacteristics} to store in + * association with self. + */ + public SwerveDriveConfiguration( + SwerveModuleConfiguration[] moduleConfigs, + SwerveIMU swerveIMU, + boolean invertedIMU, + SwerveModulePhysicalCharacteristics physicalCharacteristics) { + this.moduleCount = moduleConfigs.length; + this.imu = swerveIMU; + swerveIMU.setInverted(invertedIMU); + this.modules = createModules(moduleConfigs); + this.moduleLocationsMeters = new Translation2d[moduleConfigs.length]; + for (SwerveModule module : modules) { + this.moduleLocationsMeters[module.moduleNumber] = module.configuration.moduleLocation; + } + this.physicalCharacteristics = physicalCharacteristics; + } + + /** + * Create modules based off of the SwerveModuleConfiguration. + * + * @param swerves Swerve constants. + * @return Swerve Modules. + */ + public SwerveModule[] createModules(SwerveModuleConfiguration[] swerves) { + SwerveModule[] modArr = new SwerveModule[swerves.length]; + for (int i = 0; i < swerves.length; i++) { + modArr[i] = new SwerveModule(i, swerves[i]); + } + return modArr; + } + + /** + * Calculate the Drive Base Radius + * + * @return Drive base radius from center of robot to the farthest wheel in meters. + */ + public double getDriveBaseRadiusMeters() { + Translation2d centerOfModules = moduleLocationsMeters[0]; + + // Calculate the Center by adding all module offsets together. + for (int i = 1; i < moduleLocationsMeters.length; i++) { + centerOfModules = centerOfModules.plus(moduleLocationsMeters[i]); + } + + // Return Largest Radius + return centerOfModules.getDistance(moduleLocationsMeters[0]); + } + + /** + * Get the trackwidth of the swerve modules. + * + * @return Effective trackwdtih in Meters + */ + public double getTrackwidth() { + SwerveModuleConfiguration fr = SwerveMath.getSwerveModule(modules, true, false); + SwerveModuleConfiguration fl = SwerveMath.getSwerveModule(modules, true, true); + return fr.moduleLocation.getDistance(fl.moduleLocation); + } + + /** + * Get the tracklength of the swerve modules. + * + * @return Effective tracklength in Meters + */ + public double getTracklength() { + SwerveModuleConfiguration br = SwerveMath.getSwerveModule(modules, false, false); + SwerveModuleConfiguration bl = SwerveMath.getSwerveModule(modules, false, true); + return br.moduleLocation.getDistance(bl.moduleLocation); + } + + /** + * Get the {@link DCMotor} corresponding to the first module's configuration. + * + * @return {@link DCMotor} of the drive motor. + */ + public DCMotor getDriveMotorSim() { + SwerveModuleConfiguration fl = SwerveMath.getSwerveModule(modules, true, true); + return fl.driveMotor.getSimMotor(); + } + + /** + * Get the {@link DCMotor} corresponding to the first module configuration. + * + * @return {@link DCMotor} of the angle motor. + */ + public DCMotor getAngleMotorSim() { + SwerveModuleConfiguration fl = SwerveMath.getSwerveModule(modules, true, true); + return fl.angleMotor.getSimMotor(); + } + + /** + * Get the gyro simulation for the robot. + * + * @return {@link GyroSimulation} gyro simulation. + */ + public Supplier getGyroSim() { + if (imu instanceof Pigeon2Swerve) { + return COTS.ofPigeon2(); + } else if (imu instanceof NavXSwerve) { + return COTS.ofNav2X(); + } + return COTS.ofGenericGyro(); + } +} diff --git a/src/main/java/swervelib/parser/SwerveModuleConfiguration.java b/src/main/java/swervelib/parser/SwerveModuleConfiguration.java new file mode 100644 index 00000000..1fa3f4ef --- /dev/null +++ b/src/main/java/swervelib/parser/SwerveModuleConfiguration.java @@ -0,0 +1,141 @@ +package swervelib.parser; + +import edu.wpi.first.math.geometry.Translation2d; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.motors.SwerveMotor; +import swervelib.parser.json.modules.ConversionFactorsJson; + +/** Swerve Module configuration class which is used to configure {@link swervelib.SwerveModule}. */ +public class SwerveModuleConfiguration { + + /** + * Conversion factor for drive motor onboard PID's and angle PID's. Use {@link + * swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and {@link + * swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)} respectively to + * calculate the conversion factors. + */ + public final ConversionFactorsJson conversionFactors; + /** Angle offset in degrees for the Swerve Module. */ + public final double angleOffset; + /** Whether the absolute encoder is inverted. */ + public final boolean absoluteEncoderInverted; + /** State of inversion of the drive motor. */ + public final boolean driveMotorInverted; + /** State of inversion of the angle motor. */ + public final boolean angleMotorInverted; + /** PIDF configuration options for the angle motor closed-loop PID controller. */ + public PIDFConfig anglePIDF; + /** PIDF configuration options for the drive motor closed-loop PID controller. */ + public PIDFConfig velocityPIDF; + /** Swerve module location relative to the robot. */ + public Translation2d moduleLocation; + /** Physical characteristics of the swerve module. */ + public SwerveModulePhysicalCharacteristics physicalCharacteristics; + /** The drive motor and angle motor of this swerve module. */ + public SwerveMotor driveMotor, angleMotor; + /** The Absolute Encoder for the swerve module. */ + public SwerveAbsoluteEncoder absoluteEncoder; + /** Name for the swerve module for telemetry. */ + public String name; + /** Should do cosine compensation when not pointing correct direction;. */ + public boolean useCosineCompensator; + + /** + * Construct a configuration object for swerve modules. + * + * @param driveMotor Drive {@link SwerveMotor}. + * @param angleMotor Angle {@link SwerveMotor} + * @param absoluteEncoder Absolute encoder {@link SwerveAbsoluteEncoder}. + * @param angleOffset Absolute angle offset to 0. + * @param absoluteEncoderInverted Absolute encoder inverted. + * @param angleMotorInverted State of inversion of the angle motor. + * @param driveMotorInverted Drive motor inverted. + * @param xMeters Module location in meters from the center horizontally. + * @param yMeters Module location in meters from center vertically. + * @param anglePIDF Angle PIDF configuration. + * @param velocityPIDF Velocity PIDF configuration. + * @param physicalCharacteristics Physical characteristics of the swerve module. + * @param name The name for the swerve module. + * @param conversionFactors Conversion factors to be applied to the drive and angle motors. + * @param useCosineCompensator Should use cosineCompensation. + */ + public SwerveModuleConfiguration( + SwerveMotor driveMotor, + SwerveMotor angleMotor, + ConversionFactorsJson conversionFactors, + SwerveAbsoluteEncoder absoluteEncoder, + double angleOffset, + double xMeters, + double yMeters, + PIDFConfig anglePIDF, + PIDFConfig velocityPIDF, + SwerveModulePhysicalCharacteristics physicalCharacteristics, + boolean absoluteEncoderInverted, + boolean driveMotorInverted, + boolean angleMotorInverted, + String name, + boolean useCosineCompensator) { + this.driveMotor = driveMotor; + this.angleMotor = angleMotor; + this.conversionFactors = conversionFactors; + this.absoluteEncoder = absoluteEncoder; + this.angleOffset = angleOffset; + this.absoluteEncoderInverted = absoluteEncoderInverted; + this.driveMotorInverted = driveMotorInverted; + this.angleMotorInverted = angleMotorInverted; + this.moduleLocation = new Translation2d(xMeters, yMeters); + this.anglePIDF = anglePIDF; + this.velocityPIDF = velocityPIDF; + this.physicalCharacteristics = physicalCharacteristics; + this.name = name; + this.useCosineCompensator = useCosineCompensator; + } + + /** + * Construct a configuration object for swerve modules. Assumes the absolute encoder and drive + * motor are not inverted. + * + * @param driveMotor Drive {@link SwerveMotor}. + * @param angleMotor Angle {@link SwerveMotor} + * @param conversionFactors Conversion factors for angle/azimuth motors drive factors. + * @param absoluteEncoder Absolute encoder {@link SwerveAbsoluteEncoder}. + * @param angleOffset Absolute angle offset to 0. + * @param xMeters Module location in meters from the center horizontally. + * @param yMeters Module location in meters from center vertically. + * @param anglePIDF Angle PIDF configuration. + * @param velocityPIDF Velocity PIDF configuration. + * @param physicalCharacteristics Physical characteristics of the swerve module. + * @param name Name for the module. + * @param useCosineCompensator Should use cosineCompensation. + */ + public SwerveModuleConfiguration( + SwerveMotor driveMotor, + SwerveMotor angleMotor, + ConversionFactorsJson conversionFactors, + SwerveAbsoluteEncoder absoluteEncoder, + double angleOffset, + double xMeters, + double yMeters, + PIDFConfig anglePIDF, + PIDFConfig velocityPIDF, + SwerveModulePhysicalCharacteristics physicalCharacteristics, + String name, + boolean useCosineCompensator) { + this( + driveMotor, + angleMotor, + conversionFactors, + absoluteEncoder, + angleOffset, + xMeters, + yMeters, + anglePIDF, + velocityPIDF, + physicalCharacteristics, + false, + false, + false, + name, + useCosineCompensator); + } +} diff --git a/src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java b/src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java new file mode 100644 index 00000000..b38cc47b --- /dev/null +++ b/src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java @@ -0,0 +1,112 @@ +package swervelib.parser; + +import swervelib.parser.json.modules.ConversionFactorsJson; + +/** Configuration class which stores physical characteristics shared between every swerve module. */ +public class SwerveModulePhysicalCharacteristics { + + /** Current limits for the Swerve Module. */ + public final int driveMotorCurrentLimit, angleMotorCurrentLimit; + /** The time it takes for the motor to go from 0 to full throttle in seconds. */ + public final double driveMotorRampRate, angleMotorRampRate; + /** The minimum voltage to spin the module or wheel. */ + public final double driveFrictionVoltage, angleFrictionVoltage; + /** Wheel grip tape coefficient of friction on carpet, as described by the vendor. */ + public final double wheelGripCoefficientOfFriction; + /** Steer rotational inertia in (KilogramSquareMeters) kg/m_sq. */ + public final double steerRotationalInertia; + /** Robot mass in Kilograms. */ + public final double robotMassKg; + /** The voltage to use for the smart motor voltage compensation. */ + public double optimalVoltage; + /** + * The conversion factors for the drive and angle motors, created by {@link + * swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and {@link + * swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. + */ + public ConversionFactorsJson conversionFactor; + + /** + * Construct the swerve module physical characteristics. + * + * @param conversionFactors The conversion factors for the drive and angle motors, created by + * {@link swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and + * {@link swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. + * @param wheelGripCoefficientOfFriction Wheel grip coefficient of friction on carpet given by + * manufacturer. + * @param optimalVoltage Optimal robot voltage. + * @param driveMotorCurrentLimit Current limit for the drive motor. + * @param angleMotorCurrentLimit Current limit for the angle motor. + * @param driveMotorRampRate The time in seconds to go from 0 to full throttle on the motor. + * (Prevents over drawing power from battery) + * @param angleMotorRampRate The time in seconds to go from 0 to full throttle on the motor. + * (Prevents overdrawing power and power loss). + * @param angleFrictionVoltage Angle motor minimum voltage. + * @param driveFrictionVoltage Drive motor minimum voltage. + * @param steerRotationalInertia Steering rotational inertia in KilogramSquareMeters. + * @param robotMassKg Robot mass in kG. + */ + public SwerveModulePhysicalCharacteristics( + ConversionFactorsJson conversionFactors, + double wheelGripCoefficientOfFriction, + double optimalVoltage, + int driveMotorCurrentLimit, + int angleMotorCurrentLimit, + double driveMotorRampRate, + double angleMotorRampRate, + double driveFrictionVoltage, + double angleFrictionVoltage, + double steerRotationalInertia, + double robotMassKg) { + this.wheelGripCoefficientOfFriction = wheelGripCoefficientOfFriction; + this.optimalVoltage = optimalVoltage; + + this.conversionFactor = conversionFactors; + // Set the conversion factors to null if they are both 0. + if (conversionFactors != null) { + if (conversionFactors.isAngleEmpty() && conversionFactors.isDriveEmpty()) { + this.conversionFactor = null; + } + } + + this.driveMotorCurrentLimit = driveMotorCurrentLimit; + this.angleMotorCurrentLimit = angleMotorCurrentLimit; + this.driveMotorRampRate = driveMotorRampRate; + this.angleMotorRampRate = angleMotorRampRate; + this.driveFrictionVoltage = driveFrictionVoltage; + this.angleFrictionVoltage = angleFrictionVoltage; + this.steerRotationalInertia = steerRotationalInertia; + this.robotMassKg = robotMassKg; + } + + /** + * Construct the swerve module physical characteristics. Assume coefficient of friction is 1.19 + * (taken from blue nitrile on carpet from Studica) and optimal voltage is 12v. Assumes the drive + * motor current limit is 40A, and the angle motor current limit is 20A. + * + * @param conversionFactors The conversion factors for the drive and angle motors, created by + * {@link swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and + * {@link swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. + * @param driveMotorRampRate The time in seconds to go from 0 to full throttle on the motor. + * (Prevents over drawing power from battery) + * @param angleMotorRampRate The time in seconds to go from 0 to full throttle on the motor. + * (Prevents overdrawing power and power loss). + */ + public SwerveModulePhysicalCharacteristics( + ConversionFactorsJson conversionFactors, + double driveMotorRampRate, + double angleMotorRampRate) { + this( + conversionFactors, + 1.19, + 12, + 40, + 20, + driveMotorRampRate, + angleMotorRampRate, + 0.2, + 0.3, + 0.03, + 50); + } +} diff --git a/src/main/java/swervelib/parser/SwerveParser.java b/src/main/java/swervelib/parser/SwerveParser.java new file mode 100644 index 00000000..643cde4c --- /dev/null +++ b/src/main/java/swervelib/parser/SwerveParser.java @@ -0,0 +1,180 @@ +package swervelib.parser; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.wpi.first.math.geometry.Pose2d; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import swervelib.SwerveDrive; +import swervelib.SwerveModule; +import swervelib.math.SwerveMath; +import swervelib.parser.json.ControllerPropertiesJson; +import swervelib.parser.json.ModuleJson; +import swervelib.parser.json.PIDFPropertiesJson; +import swervelib.parser.json.PhysicalPropertiesJson; +import swervelib.parser.json.SwerveDriveJson; + +/** Helper class used to parse the JSON directory with specified configuration options. */ +public class SwerveParser { + + /** Module number mapped to the JSON name. */ + private static final HashMap moduleConfigs = new HashMap<>(); + /** Parsed swervedrive.json */ + public static SwerveDriveJson swerveDriveJson; + /** Parsed controllerproperties.json */ + public static ControllerPropertiesJson controllerPropertiesJson; + /** Parsed modules/pidfproperties.json */ + public static PIDFPropertiesJson pidfPropertiesJson; + /** Parsed modules/physicalproperties.json */ + public static PhysicalPropertiesJson physicalPropertiesJson; + /** Array holding the module jsons given in {@link SwerveDriveJson}. */ + public static ModuleJson[] moduleJsons; + + /** + * Construct a swerve parser. Will throw an error if there is a missing file. + * + * @param directory Directory with swerve configurations. + * @throws IOException if a file doesn't exist. + */ + public SwerveParser(File directory) throws IOException { + checkDirectory(directory); + swerveDriveJson = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue(new File(directory, "swervedrive.json"), SwerveDriveJson.class); + controllerPropertiesJson = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue( + new File(directory, "controllerproperties.json"), ControllerPropertiesJson.class); + pidfPropertiesJson = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue( + new File(directory, "modules/pidfproperties.json"), PIDFPropertiesJson.class); + physicalPropertiesJson = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue( + new File(directory, "modules/physicalproperties.json"), + PhysicalPropertiesJson.class); + moduleJsons = new ModuleJson[swerveDriveJson.modules.length]; + for (int i = 0; i < moduleJsons.length; i++) { + moduleConfigs.put(swerveDriveJson.modules[i], i); + File moduleFile = new File(directory, "modules/" + swerveDriveJson.modules[i]); + assert moduleFile.exists(); + moduleJsons[i] = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue(moduleFile, ModuleJson.class); + } + } + + /** + * Get the swerve module by the json name. + * + * @param name JSON name. + * @param driveConfiguration {@link SwerveDriveConfiguration} to pull from. + * @return {@link SwerveModuleConfiguration} based on the file. + */ + public static SwerveModule getModuleConfigurationByName( + String name, SwerveDriveConfiguration driveConfiguration) { + return driveConfiguration.modules[moduleConfigs.get(name + ".json")]; + } + + /** + * Open JSON file. + * + * @param file JSON File to open. + * @return JsonNode of file. + */ + private JsonNode openJson(File file) { + try { + return new ObjectMapper().readTree(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Check directory structure. + * + * @param directory JSON Configuration Directory + */ + private void checkDirectory(File directory) { + assert new File(directory, "swervedrive.json").exists(); + assert new File(directory, "controllerproperties.json").exists(); + assert new File(directory, "modules").exists() && new File(directory, "modules").isDirectory(); + assert new File(directory, "modules/pidfproperties.json").exists(); + assert new File(directory, "modules/physicalproperties.json").exists(); + } + + /** + * Create {@link SwerveDrive} from JSON configuration directory. + * + * @param maxSpeed Maximum speed of the robot in meters per second, used for both angular + * acceleration used in {@link swervelib.SwerveController} and drive feedforward in {@link + * SwerveMath#createDriveFeedforward(double, double, double)}. + * @return {@link SwerveDrive} instance. + */ + public SwerveDrive createSwerveDrive(double maxSpeed) { + return createSwerveDrive(maxSpeed, Pose2d.kZero); + } + + /** + * Create {@link SwerveDrive} from JSON configuration directory. + * + * @param maxSpeed Maximum speed of the robot in meters per second, used for both angular + * acceleration used in {@link swervelib.SwerveController} and drive feedforward in {@link + * SwerveMath#createDriveFeedforward(double, double, double)}. + * @param angleMotorConversionFactor Angle (AKA azimuth) motor conversion factor to convert motor + * controller PID loop units to degrees, usually created using {@link + * SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. + * @param driveMotorConversion Drive motor conversion factor to convert motor controller PID loop + * units to meters per rotation, usually created using {@link + * SwerveMath#calculateMetersPerRotation(double, double, double)}. + * @return {@link SwerveDrive} instance. + */ + public SwerveDrive createSwerveDrive( + double maxSpeed, double angleMotorConversionFactor, double driveMotorConversion) { + physicalPropertiesJson.conversionFactors.angle.factor = angleMotorConversionFactor; + physicalPropertiesJson.conversionFactors.drive.factor = driveMotorConversion; + return createSwerveDrive(maxSpeed, Pose2d.kZero); + } + + /** + * Create {@link SwerveDrive} from JSON configuration directory. + * + * @param maxSpeed Maximum speed of the robot in meters per second for normal+angular acceleration + * in {@link swervelib.SwerveController} of the robot + * @param initialPose {@link Pose2d} initial pose. + * @return {@link SwerveDrive} instance. + */ + public SwerveDrive createSwerveDrive(double maxSpeed, Pose2d initialPose) { + SwerveModuleConfiguration[] moduleConfigurations = + new SwerveModuleConfiguration[moduleJsons.length]; + for (int i = 0; i < moduleConfigurations.length; i++) { + ModuleJson module = moduleJsons[i]; + moduleConfigurations[i] = + module.createModuleConfiguration( + pidfPropertiesJson.angle, + pidfPropertiesJson.drive, + physicalPropertiesJson.createPhysicalProperties(), + swerveDriveJson.modules[i]); + } + SwerveDriveConfiguration swerveDriveConfiguration = + new SwerveDriveConfiguration( + moduleConfigurations, + swerveDriveJson.imu.createIMU(), + swerveDriveJson.invertedIMU, + physicalPropertiesJson.createPhysicalProperties()); + + return new SwerveDrive( + swerveDriveConfiguration, + controllerPropertiesJson.createControllerConfiguration(swerveDriveConfiguration, maxSpeed), + maxSpeed, + initialPose); + } +} diff --git a/src/main/java/swervelib/parser/deserializer/PIDFRange.java b/src/main/java/swervelib/parser/deserializer/PIDFRange.java new file mode 100644 index 00000000..57b3d3ad --- /dev/null +++ b/src/main/java/swervelib/parser/deserializer/PIDFRange.java @@ -0,0 +1,10 @@ +package swervelib.parser.deserializer; + +/** Class to hold the minimum and maximum input or output of the PIDF. */ +public class PIDFRange { + + /** Minimum value. */ + public double min = -1; + /** Maximum value. */ + public double max = 1; +} diff --git a/src/main/java/swervelib/parser/deserializer/ReflectionsManager.java b/src/main/java/swervelib/parser/deserializer/ReflectionsManager.java new file mode 100644 index 00000000..ec10b8d5 --- /dev/null +++ b/src/main/java/swervelib/parser/deserializer/ReflectionsManager.java @@ -0,0 +1,82 @@ +package swervelib.parser.deserializer; + +/** Create classes only if the vendor dep exists. */ +public class ReflectionsManager { + + /** Vendors that supply their own vendordep to communicate with their products. */ + public enum VENDOR { + /** REVLib */ + REV, + /** CTRE Phoenix 5 and 6 */ + PHOENIX5, + PHOENIX6, + /** ThriftyLib */ + THRIFTYBOT, + /** StudicaLib */ + STUDICA + } + + /** + * Check if the vendordep exists. + * + * @param vendor Vendor to check for their library. + * @return Boolean on existence of their library. + */ + public static boolean checkIfVendorLibExists(VENDOR vendor) { + try { + Class library; + switch (vendor) { + case REV -> { + library = Class.forName("com.revrobotics.spark.SparkBase"); + break; + } + case PHOENIX6 -> { + library = Class.forName("com.ctre.phoenix6.hardware.TalonFXS"); + break; + } + case PHOENIX5 -> { + library = Class.forName("com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX"); + break; + } + case THRIFTYBOT -> { + library = Class.forName("com.thethriftybot.ThriftyNova"); + break; + } + case STUDICA -> { + library = Class.forName("com.studica.frc.AHRS"); + break; + } + default -> { + return false; + } + } + } catch (Exception e) { + return false; + } + return true; + } + + /** + * Create objects if the vendordep exists. Throw an exception when they dont. + * + * @param v Vendor to check if the vendordep exists. + * @param className Wrapper classname to create. + * @param parameterTypes Parameter types for the wrappers constructor. + * @param parameters Parameters for the wrappers constructor + * @param Wrapper type. + * @return Wrapper object. + */ + public static T create( + VENDOR v, String className, Class[] parameterTypes, Object[] parameters) { + if (!checkIfVendorLibExists(v)) { + throw new RuntimeException("Vendor " + v + " library not found! Please install it!"); + } + try { + Class wrapper = Class.forName(className); + Object vendorObj = wrapper.getDeclaredConstructor(parameterTypes).newInstance(parameters); + return (T) vendorObj; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/swervelib/parser/deserializer/package-info.java b/src/main/java/swervelib/parser/deserializer/package-info.java new file mode 100644 index 00000000..8a176651 --- /dev/null +++ b/src/main/java/swervelib/parser/deserializer/package-info.java @@ -0,0 +1,2 @@ +/** Deserialize specific variables for outside the parser. */ +package swervelib.parser.deserializer; diff --git a/src/main/java/swervelib/parser/json/ControllerPropertiesJson.java b/src/main/java/swervelib/parser/json/ControllerPropertiesJson.java new file mode 100644 index 00000000..1b767faa --- /dev/null +++ b/src/main/java/swervelib/parser/json/ControllerPropertiesJson.java @@ -0,0 +1,30 @@ +package swervelib.parser.json; + +import swervelib.parser.PIDFConfig; +import swervelib.parser.SwerveControllerConfiguration; +import swervelib.parser.SwerveDriveConfiguration; + +/** {@link swervelib.SwerveController} parsed class. Used to access the JSON data. */ +public class ControllerPropertiesJson { + + /** + * The minimum radius of the angle control joystick to allow for heading adjustment of the robot. + */ + public double angleJoystickRadiusDeadband; + /** The PID used to control the robot heading. */ + public PIDFConfig heading; + + /** + * Create the {@link SwerveControllerConfiguration} based on parsed and given data. + * + * @param driveConfiguration {@link SwerveDriveConfiguration} parsed configuration. + * @param maxSpeedMPS Maximum speed in meters per second for the angular acceleration of the + * robot. + * @return {@link SwerveControllerConfiguration} object based on parsed data. + */ + public SwerveControllerConfiguration createControllerConfiguration( + SwerveDriveConfiguration driveConfiguration, double maxSpeedMPS) { + return new SwerveControllerConfiguration( + driveConfiguration, heading, angleJoystickRadiusDeadband, maxSpeedMPS); + } +} diff --git a/src/main/java/swervelib/parser/json/DeviceJson.java b/src/main/java/swervelib/parser/json/DeviceJson.java new file mode 100644 index 00000000..ab695d28 --- /dev/null +++ b/src/main/java/swervelib/parser/json/DeviceJson.java @@ -0,0 +1,301 @@ +package swervelib.parser.json; + +import static swervelib.telemetry.SwerveDriveTelemetry.canIdWarning; +import static swervelib.telemetry.SwerveDriveTelemetry.i2cLockupWarning; +import static swervelib.telemetry.SwerveDriveTelemetry.serialCommsIssueWarning; + +import com.ctre.phoenix.motorcontrol.FeedbackDevice; +import com.studica.frc.AHRS.NavXComType; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.wpilibj.DriverStation; +import swervelib.encoders.AnalogAbsoluteEncoderSwerve; +import swervelib.encoders.CANCoderSwerve; +import swervelib.encoders.CanAndMagSwerve; +import swervelib.encoders.DIODutyCycleEncoderSwerve; +import swervelib.encoders.SparkFlexEncoderSwerve; +import swervelib.encoders.SparkMaxAnalogEncoderSwerve; +import swervelib.encoders.SparkMaxEncoderSwerve; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.encoders.TalonSRXEncoderSwerve; +import swervelib.imu.ADIS16448Swerve; +import swervelib.imu.ADIS16470Swerve; +import swervelib.imu.ADXRS450Swerve; +import swervelib.imu.AnalogGyroSwerve; +import swervelib.imu.CanandgyroSwerve; +import swervelib.imu.NavXSwerve; +import swervelib.imu.Pigeon2Swerve; +import swervelib.imu.PigeonSwerve; +import swervelib.imu.PigeonViaTalonSRXSwerve; +import swervelib.imu.SwerveIMU; +import swervelib.motors.SparkFlexSwerve; +import swervelib.motors.SparkMaxBrushedMotorSwerve; +import swervelib.motors.SparkMaxBrushedMotorSwerve.Type; +import swervelib.motors.SparkMaxSwerve; +import swervelib.motors.SwerveMotor; +import swervelib.motors.TalonFXSSwerve; +import swervelib.motors.TalonFXSwerve; +import swervelib.motors.TalonSRXSwerve; +import swervelib.parser.deserializer.ReflectionsManager; +import swervelib.parser.deserializer.ReflectionsManager.VENDOR; + +/** Device JSON parsed class. Used to access the JSON data. */ +public class DeviceJson { + + /** The device type, e.g. pigeon/pigeon2/sparkmax/talonfx/navx */ + public String type; + /** The CAN ID or pin ID of the device. */ + public int id; + /** The CAN bus name which the device resides on if using CAN. */ + public String canbus = ""; + + /** + * Create a {@link SwerveAbsoluteEncoder} from the current configuration. + * + * @param motor {@link SwerveMotor} of which attached encoders will be created from, only used + * when the type is "attached" or "canandencoder". + * @return {@link SwerveAbsoluteEncoder} given. + */ + public SwerveAbsoluteEncoder createEncoder(SwerveMotor motor) { + if (id > 40) { + canIdWarning.set(true); + } + switch (type) { + case "none": + return null; + case "integrated": + case "attached": + case "canandmag": + case "canandcoder": + return new SparkMaxEncoderSwerve(motor, 360); + case "sparkmax_analog": + return new SparkMaxAnalogEncoderSwerve(motor, 3.3); + case "sparkmax_analog5v": + return new SparkMaxAnalogEncoderSwerve(motor, 5); + case "sparkflex_integrated": + case "sparkflex_attached": + case "sparkflex_canandmag": + case "sparkflex_canandcoder": + return new SparkFlexEncoderSwerve(motor, 360); + case "canandcoder_can": + case "canandmag_can": + return new CanAndMagSwerve(id); + case "ctre_mag": + case "rev_hex": + case "throughbore": + case "am_mag": + case "dutycycle": + return new DIODutyCycleEncoderSwerve(id); + case "thrifty": + case "ma3": + case "analog": + return new AnalogAbsoluteEncoderSwerve(id); + case "cancoder": + return new CANCoderSwerve(id, canbus != null ? canbus : ""); + case "srxmag_standalone": + return new TalonSRXEncoderSwerve( + new TalonSRXSwerve(id, false, DCMotor.getCIM(1)), + FeedbackDevice.PulseWidthEncodedPosition); + case "talonsrx_pwm": + return new TalonSRXEncoderSwerve(motor, FeedbackDevice.PulseWidthEncodedPosition); + case "talonsrx_analog": + return new TalonSRXEncoderSwerve(motor, FeedbackDevice.Analog); + case "thrifty_nova_rev": + return ReflectionsManager.create( + VENDOR.THRIFTYBOT, + "swervelib.encoders.ThriftyNovaEncoderSwerve", + new Class[] {SwerveMotor.class, String.class}, + new Object[] {motor, "REV_ENCODER"}); + case "thrifty_nova_redux": + return ReflectionsManager.create( + VENDOR.THRIFTYBOT, + "swervelib.encoders.ThriftyNovaEncoderSwerve", + new Class[] {SwerveMotor.class, String.class}, + new Object[] {motor, "REDUX_ENCODER"}); + case "thrifty_nova_srx_mag": + return ReflectionsManager.create( + VENDOR.THRIFTYBOT, + "swervelib.encoders.ThriftyNovaEncoderSwerve", + new Class[] {SwerveMotor.class, String.class}, + new Object[] {motor, "SRX_MAG_ENCODER"}); + default: + throw new RuntimeException(type + " is not a recognized absolute encoder type."); + } + } + + /** + * Create a {@link SwerveIMU} from the given configuration. + * + * @return {@link SwerveIMU} given. + */ + public SwerveIMU createIMU() { + if (id > 40) { + canIdWarning.set(true); + } + switch (type) { + case "adis16448": + return new ADIS16448Swerve(); + case "adis16470": + return new ADIS16470Swerve(); + case "adxrs450": + return new ADXRS450Swerve(); + case "analog": + return new AnalogGyroSwerve(id); + case "canandgyro": + return new CanandgyroSwerve(id); + case "navx": + case "navx_spi": + return new NavXSwerve(NavXComType.kMXP_SPI); + case "navx_i2c": + DriverStation.reportWarning( + "WARNING: There exists an I2C lockup issue on the roboRIO that could occur, more information here: " + + "\nhttps://docs.wpilib.org/en/stable/docs/yearly-overview/known-issues" + + ".html#onboard-i2c-causing-system-lockups", + false); + i2cLockupWarning.set(true); + return new NavXSwerve(NavXComType.kI2C); + case "navx_usb": + DriverStation.reportWarning( + "WARNING: There is issues when using USB camera's and the NavX like this!\n" + + "https://pdocs.kauailabs.com/navx-mxp/guidance/selecting-an-interface/", + false); + serialCommsIssueWarning.set(true); + return new NavXSwerve(NavXComType.kUSB1); + case "navx_mxp_serial": + serialCommsIssueWarning.set(true); + return new NavXSwerve(NavXComType.kMXP_UART); + case "pigeon": + return new PigeonSwerve(id); + case "pigeon_via_talonsrx": + return new PigeonViaTalonSRXSwerve(id); + case "pigeon2": + return new Pigeon2Swerve(id, canbus != null ? canbus : ""); + default: + throw new RuntimeException(type + " is not a recognized imu/gyroscope type."); + } + } + + /** + * Create a {@link SwerveMotor} from the given configuration. + * + * @param isDriveMotor If the motor being generated is a drive motor. + * @return {@link SwerveMotor} given. + */ + public SwerveMotor createMotor(boolean isDriveMotor) { + if (id > 40) { + canIdWarning.set(true); + } + switch (type) { + case "talonfxs_neo": + return new TalonFXSSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getNEO(1)); + case "talonfxs_neo550": + return new TalonFXSSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getNeo550(1)); + case "talonfxs_vortex": + return new TalonFXSSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getNeoVortex(1)); + case "talonfxs_minion": + throw new UnsupportedOperationException( + "Cannot create minion combination yet"); // new TalonFXSSwerve(id, canbus != null ? + // canbus : "", isDriveMotor, + // DCMotor.getNeoVortex(1)); + case "sparkmax_neo": + case "neo": + case "sparkmax": + return new SparkMaxSwerve(id, isDriveMotor, DCMotor.getNEO(1)); + case "sparkmax_vortex": + return new SparkMaxSwerve(id, isDriveMotor, DCMotor.getNeoVortex(1)); + case "sparkmax_minion": + throw new UnsupportedOperationException("Cannot create minion combination yet"); + case "sparkmax_neo550": + case "neo550": + return new SparkMaxSwerve(id, isDriveMotor, DCMotor.getNeo550(1)); + case "sparkflex_vortex": + case "vortex": + case "sparkflex": + return new SparkFlexSwerve(id, isDriveMotor, DCMotor.getNeoVortex(1)); + case "sparkflex_neo": + return new SparkFlexSwerve(id, isDriveMotor, DCMotor.getNEO(1)); + case "sparkflex_neo550": + return new SparkFlexSwerve(id, isDriveMotor, DCMotor.getNeo550(1)); + case "sparkflex_minion": + throw new UnsupportedOperationException("Cannot create minion combination yet"); + case "falcon500": + case "falcon": + return new TalonFXSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getFalcon500(1)); + case "falcon500foc": + return new TalonFXSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getFalcon500Foc(1)); + case "krakenx60": + case "talonfx": + return new TalonFXSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getKrakenX60(1)); + case "krakenx60foc": + return new TalonFXSwerve( + id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getKrakenX60Foc(1)); + case "talonsrx": + return new TalonSRXSwerve(id, isDriveMotor, DCMotor.getCIM(1)); + case "sparkmax_brushed": + if (canbus == null) { + canbus = ""; + } + switch (canbus) { + case "greyhill_63r256": + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kQuadrature, 1024, false, DCMotor.getCIM(1)); + case "srx_mag_encoder": + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kQuadrature, 4096, false, DCMotor.getCIM(1)); + case "throughbore": + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kQuadrature, 8192, false, DCMotor.getCIM(1)); + case "throughbore_dataport": + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kNoSensor, 8192, true, DCMotor.getCIM(1)); + case "greyhill_63r256_dataport": + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kQuadrature, 1024, true, DCMotor.getCIM(1)); + case "srx_mag_encoder_dataport": + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kQuadrature, 4096, true, DCMotor.getCIM(1)); + default: + if (isDriveMotor) { + throw new RuntimeException( + "Spark MAX " + id + " MUST have a encoder attached to the motor controller."); + } + // We are creating a motor for an angle motor which will use the absolute encoder + // attached to the data port. + return new SparkMaxBrushedMotorSwerve( + id, isDriveMotor, Type.kNoSensor, 0, false, DCMotor.getCIM(1)); + } + case "nova_neo": + return ReflectionsManager.create( + VENDOR.THRIFTYBOT, + "swervelib.motors.ThriftyNovaSwerve", + new Class[] {int.class, boolean.class, DCMotor.class}, + new Object[] {id, isDriveMotor, DCMotor.getNEO(1)}); + + case "nova_neo550": + return ReflectionsManager.create( + VENDOR.THRIFTYBOT, + "swervelib.motors.ThriftyNovaSwerve", + new Class[] {int.class, boolean.class, DCMotor.class}, + new Object[] {id, isDriveMotor, DCMotor.getNeo550(1)}); + + case "nova_vortex": + return ReflectionsManager.create( + VENDOR.THRIFTYBOT, + "swervelib.motors.ThriftyNovaSwerve", + new Class[] {int.class, boolean.class, DCMotor.class}, + new Object[] {id, isDriveMotor, DCMotor.getNeoVortex(1)}); + + case "nova_minion": + throw new UnsupportedOperationException( + "Cannot create minion combination"); // return new ThriftyNovaSwerve(id, isDriveMotor, + // DCMotor.getMinion(1)); + default: + throw new RuntimeException(type + " is not a recognized motor type."); + } + } +} diff --git a/src/main/java/swervelib/parser/json/ModuleJson.java b/src/main/java/swervelib/parser/json/ModuleJson.java new file mode 100644 index 00000000..d0d70bf8 --- /dev/null +++ b/src/main/java/swervelib/parser/json/ModuleJson.java @@ -0,0 +1,122 @@ +package swervelib.parser.json; + +import com.revrobotics.spark.SparkMax; +import edu.wpi.first.math.util.Units; +import swervelib.encoders.SparkMaxEncoderSwerve; +import swervelib.encoders.SwerveAbsoluteEncoder; +import swervelib.encoders.ThriftyNovaEncoderSwerve; +import swervelib.motors.SwerveMotor; +import swervelib.motors.ThriftyNovaSwerve; +import swervelib.parser.PIDFConfig; +import swervelib.parser.SwerveModuleConfiguration; +import swervelib.parser.SwerveModulePhysicalCharacteristics; +import swervelib.parser.json.modules.BoolMotorJson; +import swervelib.parser.json.modules.ConversionFactorsJson; +import swervelib.parser.json.modules.LocationJson; + +/** {@link swervelib.SwerveModule} JSON parsed class. Used to access the JSON data. */ +public class ModuleJson { + + /** Drive motor device configuration. */ + public DeviceJson drive; + /** Angle motor device configuration. */ + public DeviceJson angle; + /** Conversion Factors composition. Auto-calculates the conversion factors. */ + public ConversionFactorsJson conversionFactors = new ConversionFactorsJson(); + /** Absolute encoder device configuration. */ + public DeviceJson encoder; + /** Defines which motors are inverted. */ + public BoolMotorJson inverted; + /** Absolute encoder offset from 0 in degrees. */ + public double absoluteEncoderOffset; + /** Absolute encoder inversion state. */ + public boolean absoluteEncoderInverted = false; + /** The location of the swerve module from the center of the robot in inches. */ + public LocationJson location; + /** Should do cosine compensation when not pointing correct direction;. */ + public boolean useCosineCompensator = true; + + /** + * Create the swerve module configuration based off of parsed data. + * + * @param anglePIDF The PIDF values for the angle motor. + * @param velocityPIDF The velocity PIDF values for the drive motor. + * @param physicalCharacteristics Physical characteristics of the swerve module. + * @param name Module json filename. + * @return {@link SwerveModuleConfiguration} based on the provided data and parsed data. + */ + public SwerveModuleConfiguration createModuleConfiguration( + PIDFConfig anglePIDF, + PIDFConfig velocityPIDF, + SwerveModulePhysicalCharacteristics physicalCharacteristics, + String name) { + SwerveMotor angleMotor = angle.createMotor(false); + SwerveAbsoluteEncoder absEncoder = encoder.createEncoder(angleMotor); + + // Throw an error if module locations are improperly set + if (location.front == 0 && location.left == 0) { + throw new RuntimeException( + "Improper Module Location Settings!\n" + + "Your module location is set to 0 for both 'front' and 'left' values.\n" + + "Set the distance from the center of the robot to the center of the wheel in your module JSON file!"); + } + + // Set the conversion factors to null if they are both 0. + if (!conversionFactors.works() && physicalCharacteristics.conversionFactor == null) { + throw new RuntimeException( + "No Conversion Factor configured! Please create SwerveDrive using \n" + + "SwerveParser.createSwerveDrive(driveFeedforward, maxSpeed, angleMotorConversionFactor, driveMotorConversion)\n" + + "OR\n" + + "SwerveParser.createSwerveDrive(maxSpeed, angleMotorConversionFactor, driveMotorConversion)\n" + + "OR\n" + + "Set the conversion factor in physicalproperties.json OR the module JSON file." + + "REMEMBER: You can calculate the conversion factors using SwerveMath.calculateMetersPerRotation AND SwerveMath.calculateDegreesPerSteeringRotation\n"); + } else if (physicalCharacteristics.conversionFactor.works() && !conversionFactors.works()) { + conversionFactors = physicalCharacteristics.conversionFactor; + } else if (physicalCharacteristics.conversionFactor.works()) + // If both are defined, override 0 with the physical characterstics input. + { + conversionFactors.angle = + conversionFactors.isAngleEmpty() + ? physicalCharacteristics.conversionFactor.angle + : conversionFactors.angle; + conversionFactors.drive = + conversionFactors.isDriveEmpty() + ? physicalCharacteristics.conversionFactor.drive + : conversionFactors.drive; + } + + if (conversionFactors.isDriveEmpty() || conversionFactors.isAngleEmpty()) { + throw new RuntimeException( + "Conversion factors cannot be 0, please configure conversion factors in physicalproperties.json or the module JSON files."); + } + + // Backwards compatibility, auto-optimization. + if (conversionFactors.angle.factor == 360 + && absEncoder != null + && (absEncoder instanceof SparkMaxEncoderSwerve + && angleMotor.getMotor() instanceof SparkMax)) { + angleMotor.setAbsoluteEncoder(absEncoder); + } else if ((absEncoder instanceof ThriftyNovaEncoderSwerve + && angleMotor instanceof ThriftyNovaSwerve)) { + angleMotor.setAbsoluteEncoder(absEncoder); + } + + return new SwerveModuleConfiguration( + drive.createMotor(true), + angleMotor, + conversionFactors, + absEncoder, + absoluteEncoderOffset, + Units.inchesToMeters(Math.round(location.front)), + Units.inchesToMeters(Math.round(location.left)), + anglePIDF, + velocityPIDF, + physicalCharacteristics, + absoluteEncoderInverted, + inverted.drive, + inverted.angle, + name.replaceAll("\\.json", ""), + useCosineCompensator); + } +} diff --git a/src/main/java/swervelib/parser/json/MotorConfigDouble.java b/src/main/java/swervelib/parser/json/MotorConfigDouble.java new file mode 100644 index 00000000..fa0d1fc9 --- /dev/null +++ b/src/main/java/swervelib/parser/json/MotorConfigDouble.java @@ -0,0 +1,24 @@ +package swervelib.parser.json; + +/** Used to store doubles for motor configuration. */ +public class MotorConfigDouble { + + /** Drive motor. */ + public double drive; + /** Angle motor. */ + public double angle; + + /** Default constructor. */ + public MotorConfigDouble() {} + + /** + * Default constructor. + * + * @param angle Angle data. + * @param drive Drive data. + */ + public MotorConfigDouble(double angle, double drive) { + this.angle = angle; + this.drive = drive; + } +} diff --git a/src/main/java/swervelib/parser/json/MotorConfigInt.java b/src/main/java/swervelib/parser/json/MotorConfigInt.java new file mode 100644 index 00000000..6d191386 --- /dev/null +++ b/src/main/java/swervelib/parser/json/MotorConfigInt.java @@ -0,0 +1,24 @@ +package swervelib.parser.json; + +/** Used to store ints for motor configuration. */ +public class MotorConfigInt { + + /** Drive motor. */ + public int drive; + /** Angle motor. */ + public int angle; + + /** Default constructor. */ + public MotorConfigInt() {} + + /** + * Default constructor with values. + * + * @param drive Drive data. + * @param angle Angle data. + */ + public MotorConfigInt(int drive, int angle) { + this.angle = angle; + this.drive = drive; + } +} diff --git a/src/main/java/swervelib/parser/json/PIDFPropertiesJson.java b/src/main/java/swervelib/parser/json/PIDFPropertiesJson.java new file mode 100644 index 00000000..929dea40 --- /dev/null +++ b/src/main/java/swervelib/parser/json/PIDFPropertiesJson.java @@ -0,0 +1,12 @@ +package swervelib.parser.json; + +import swervelib.parser.PIDFConfig; + +/** {@link swervelib.SwerveModule} PID with Feedforward for the drive motor and angle motor. */ +public class PIDFPropertiesJson { + + /** The PIDF with Integral Zone used for the drive motor. */ + public PIDFConfig drive; + /** The PIDF with Integral Zone used for the angle motor. */ + public PIDFConfig angle; +} diff --git a/src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java b/src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java new file mode 100644 index 00000000..0b783f19 --- /dev/null +++ b/src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java @@ -0,0 +1,75 @@ +package swervelib.parser.json; + +import static edu.wpi.first.units.Units.Kilogram; +import static edu.wpi.first.units.Units.Pounds; + +import swervelib.parser.SwerveModulePhysicalCharacteristics; +import swervelib.parser.json.modules.ConversionFactorsJson; + +/** + * {@link swervelib.parser.SwerveModulePhysicalCharacteristics} parsed data. Used to configure the + * SwerveModule. + */ +public class PhysicalPropertiesJson { + + /** DEPRECATED! Use {@link PhysicalPropertiesJson#conversionFactors} instead. */ + @Deprecated(since = "2025", forRemoval = true) + public MotorConfigDouble conversionFactor = new MotorConfigDouble(); + /** Minimum voltage to spin the module or wheel. */ + public MotorConfigDouble friction = new MotorConfigDouble(0.3, 0.2); + /** Steer rotational inertia in KilogramMetersSquare. */ + public double steerRotationalInertia = 0.03; + /** Robot mass in lb (pounds) */ + public double robotMass = 110.2311; + /** Conversion Factors composition. Auto-calculates the conversion factors. */ + public ConversionFactorsJson conversionFactors = new ConversionFactorsJson(); + /** The current limit in AMPs to apply to the motors. */ + public MotorConfigInt currentLimit = new MotorConfigInt(40, 20); + /** The minimum number of seconds to take for the motor to go from 0 to full throttle. */ + public MotorConfigDouble rampRate = new MotorConfigDouble(0.25, 0.25); + /** + * The grip tape coefficient of friction on carpet. Used to calculate the practical maximum + * acceleration. + */ + public double wheelGripCoefficientOfFriction = 1.19; + /** The voltage to use for the smart motor voltage compensation, default is 12. */ + public double optimalVoltage = 12; + + /** + * Create the physical characteristics based off the parsed data. + * + * @return {@link SwerveModulePhysicalCharacteristics} based on parsed data. + */ + public SwerveModulePhysicalCharacteristics createPhysicalProperties() { + // Setup deprecation notice. + if (conversionFactor.drive != 0 + && conversionFactor.angle != 0 + && conversionFactors.isDriveEmpty() + && conversionFactors.isAngleEmpty()) { + throw new RuntimeException( + "\n'conversionFactor': {'drive': " + + conversionFactor.drive + + ", 'angle': " + + conversionFactor.angle + + "} \nis deprecated, please use\n" + + "'conversionFactors': {'drive': {'factor': " + + conversionFactor.drive + + "}, 'angle': {'factor': " + + conversionFactor.angle + + "} }"); + } + + return new SwerveModulePhysicalCharacteristics( + conversionFactors, + wheelGripCoefficientOfFriction, + optimalVoltage, + currentLimit.drive, + currentLimit.angle, + rampRate.drive, + rampRate.angle, + friction.drive, + friction.angle, + steerRotationalInertia, + Pounds.of(robotMass).in(Kilogram)); + } +} diff --git a/src/main/java/swervelib/parser/json/SwerveDriveJson.java b/src/main/java/swervelib/parser/json/SwerveDriveJson.java new file mode 100644 index 00000000..0df3e695 --- /dev/null +++ b/src/main/java/swervelib/parser/json/SwerveDriveJson.java @@ -0,0 +1,15 @@ +package swervelib.parser.json; + +/** + * {@link swervelib.SwerveDrive} JSON parsed class. Used to access parsed data from the + * swervedrive.json file. + */ +public class SwerveDriveJson { + + /** Robot IMU used to determine heading of the robot. */ + public DeviceJson imu; + /** Invert the IMU of the robot. */ + public boolean invertedIMU; + /** Module JSONs in order clockwise order starting from front left. */ + public String[] modules; +} diff --git a/src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java b/src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java new file mode 100644 index 00000000..f946f664 --- /dev/null +++ b/src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java @@ -0,0 +1,27 @@ +package swervelib.parser.json.modules; + +import swervelib.math.SwerveMath; + +/** Angle motor conversion factors composite JSON parse class. */ +public class AngleConversionFactorsJson { + + /** + * Gear ratio for the angle/steering/azimuth motor on the Swerve Module. Motor rotations to 1 + * wheel rotation. + */ + public double gearRatio; + /** Calculated or given conversion factor. */ + public double factor = 0; + + /** + * Calculate the drive conversion factor. + * + * @return Drive conversion factor, if factor isn't set. + */ + public double calculate() { + if (factor == 0) { + factor = SwerveMath.calculateDegreesPerSteeringRotation(gearRatio); + } + return factor; + } +} diff --git a/src/main/java/swervelib/parser/json/modules/BoolMotorJson.java b/src/main/java/swervelib/parser/json/modules/BoolMotorJson.java new file mode 100644 index 00000000..16dd1d8b --- /dev/null +++ b/src/main/java/swervelib/parser/json/modules/BoolMotorJson.java @@ -0,0 +1,10 @@ +package swervelib.parser.json.modules; + +/** Inverted motor JSON parsed class. Used to access the JSON data. */ +public class BoolMotorJson { + + /** Drive motor inversion state. */ + public boolean drive; + /** Angle motor inversion state. */ + public boolean angle; +} diff --git a/src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java b/src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java new file mode 100644 index 00000000..8d69d3f9 --- /dev/null +++ b/src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java @@ -0,0 +1,40 @@ +package swervelib.parser.json.modules; + +/** Conversion Factors parsed JSON class */ +public class ConversionFactorsJson { + + /** Drive motor conversion factors composition. */ + public DriveConversionFactorsJson drive = new DriveConversionFactorsJson(); + /** Angle motor conversion factors composition. */ + public AngleConversionFactorsJson angle = new AngleConversionFactorsJson(); + + /** + * Check if the conversion factors are set for the drive motor. + * + * @return Empty + */ + public boolean isDriveEmpty() { + drive.calculate(); + return drive.factor == 0; + } + + /** + * Check if the conversion factors are set for the angle motor. + * + * @return Empty + */ + public boolean isAngleEmpty() { + angle.calculate(); + return angle.factor == 0; + } + + /** + * Check if the conversion factor can be found. + * + * @return If the conversion factors can be found. + */ + public boolean works() { + return (angle.factor != 0 && drive.factor != 0) + || ((drive.gearRatio != 0 && drive.diameter != 0)) && (angle.gearRatio != 0); + } +} diff --git a/src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java b/src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java new file mode 100644 index 00000000..55144f77 --- /dev/null +++ b/src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java @@ -0,0 +1,29 @@ +package swervelib.parser.json.modules; + +import edu.wpi.first.math.util.Units; +import swervelib.math.SwerveMath; + +/** Drive motor composite JSON parse class. */ +public class DriveConversionFactorsJson { + + /** Gear ratio for the drive motor rotations to turn the wheel 1 complete rotation. */ + public double gearRatio; + /** Diameter of the wheel in inches. */ + public double diameter; + /** Calculated conversion factor. */ + public double factor = 0; + + /** + * Calculate the drive conversion factor. + * + * @return Drive conversion factor, if factor isn't set. + */ + public double calculate() { + if (factor == 0) { + factor = + SwerveMath.calculateMetersPerRotation( + Units.inchesToMeters(this.diameter), this.gearRatio); + } + return factor; + } +} diff --git a/src/main/java/swervelib/parser/json/modules/LocationJson.java b/src/main/java/swervelib/parser/json/modules/LocationJson.java new file mode 100644 index 00000000..d7953258 --- /dev/null +++ b/src/main/java/swervelib/parser/json/modules/LocationJson.java @@ -0,0 +1,14 @@ +package swervelib.parser.json.modules; + +/** + * Location JSON parsed class. Used to access the JSON data. Module locations, in inches, as + * distances to the center of the robot. +x is towards the robot front, and +y is towards robot + * left. + */ +public class LocationJson { + + /** Location of the swerve module in inches from the center of the robot horizontally. */ + public double front = 0; + /** Location of the swerve module in inches from the center of the robot vertically. */ + public double left = 0; +} diff --git a/src/main/java/swervelib/parser/json/modules/package-info.java b/src/main/java/swervelib/parser/json/modules/package-info.java new file mode 100644 index 00000000..f6e0ab73 --- /dev/null +++ b/src/main/java/swervelib/parser/json/modules/package-info.java @@ -0,0 +1,2 @@ +/** JSON Mapped Configuration types for modules. */ +package swervelib.parser.json.modules; diff --git a/src/main/java/swervelib/parser/json/package-info.java b/src/main/java/swervelib/parser/json/package-info.java new file mode 100644 index 00000000..776e4150 --- /dev/null +++ b/src/main/java/swervelib/parser/json/package-info.java @@ -0,0 +1,2 @@ +/** JSON Mapped classes for parsing configuration files. */ +package swervelib.parser.json; diff --git a/src/main/java/swervelib/parser/package-info.java b/src/main/java/swervelib/parser/package-info.java new file mode 100644 index 00000000..60372be5 --- /dev/null +++ b/src/main/java/swervelib/parser/package-info.java @@ -0,0 +1,2 @@ +/** JSON Parser for YAGSL configurations. */ +package swervelib.parser; diff --git a/src/main/java/swervelib/simulation/SwerveIMUSimulation.java b/src/main/java/swervelib/simulation/SwerveIMUSimulation.java new file mode 100644 index 00000000..2bc2ef17 --- /dev/null +++ b/src/main/java/swervelib/simulation/SwerveIMUSimulation.java @@ -0,0 +1,98 @@ +package swervelib.simulation; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.kinematics.SwerveDriveKinematics; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.wpilibj.smartdashboard.Field2d; +import java.util.Optional; +import org.ironmaple.simulation.drivesims.GyroSimulation; + +/** Simulation for {@link swervelib.SwerveDrive} IMU. */ +public class SwerveIMUSimulation { + + private final GyroSimulation gyroSimulation; + + /** + * Create the swerve drive IMU simulation. + * + * @param gyroSimulation Gyro simulation from MapleSim. + */ + public SwerveIMUSimulation(GyroSimulation gyroSimulation) { + this.gyroSimulation = gyroSimulation; + } + + /** + * Get the estimated angle of the robot. + * + * @return {@link Rotation2d} estimation of the robot. + */ + public Rotation2d getYaw() { + return gyroSimulation.getGyroReading(); + } + + /** + * Pitch is not simulated currently, always returns 0. + * + * @return Pitch of the robot as {@link Rotation2d}. + */ + public Rotation2d getPitch() { + return new Rotation2d(); + } + + /** + * Roll is not simulated currently, always returns 0. + * + * @return Roll of the robot as {@link Rotation2d}. + */ + public Rotation2d getRoll() { + return new Rotation2d(); + } + + /** + * Gets the estimated gyro {@link Rotation3d} of the robot. + * + * @return The heading as a {@link Rotation3d} angle + */ + public Rotation3d getGyroRotation3d() { + return new Rotation3d(0, 0, getYaw().getRadians()); + } + + /** + * Fetch the acceleration [x, y, z] from the IMU in m/s/s. If acceleration isn't supported returns + * empty. + * + * @return {@link Translation3d} of the acceleration as an {@link Optional}. + */ + public Optional getAccel() { + return Optional.empty(); + } + + /** + * Update the odometry of the simulated {@link swervelib.SwerveDrive} and post the {@link + * swervelib.SwerveModule} states to the {@link Field2d}. + * + * @param kinematics {@link SwerveDriveKinematics} of the swerve drive. + * @param states {@link SwerveModuleState} array of the module states. + * @param modulePoses {@link Pose2d} representing the swerve modules. + * @param field {@link Field2d} to update. + */ + public void updateOdometry( + SwerveDriveKinematics kinematics, + SwerveModuleState[] states, + Pose2d[] modulePoses, + Field2d field) { + field.getObject("XModules").setPoses(modulePoses); + } + + /** + * Set the heading of the robot. + * + * @param angle Angle of the robot in radians. + */ + public void setAngle(double angle) { + this.gyroSimulation.setRotation(Rotation2d.fromRadians(angle)); + } +} diff --git a/src/main/java/swervelib/simulation/SwerveModuleSimulation.java b/src/main/java/swervelib/simulation/SwerveModuleSimulation.java new file mode 100644 index 00000000..4b147d32 --- /dev/null +++ b/src/main/java/swervelib/simulation/SwerveModuleSimulation.java @@ -0,0 +1,91 @@ +package swervelib.simulation; + +import static edu.wpi.first.units.Units.Amps; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.SwerveModulePosition; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import org.ironmaple.simulation.drivesims.SelfControlledSwerveDriveSimulation; +import swervelib.SwerveDrive; +import swervelib.parser.SwerveModulePhysicalCharacteristics; + +/** Class that wraps around {@link org.ironmaple.simulation.drivesims.SwerveModuleSimulation} */ +public class SwerveModuleSimulation { + + /** MapleSim module. */ + public SelfControlledSwerveDriveSimulation.SelfControlledModuleSimulation mapleSimModule = null; + + /** + * Configure the maple sim module + * + * @param simModule the {@link org.ironmaple.simulation.drivesims.SwerveModuleSimulation} object + * for simulation + * @param physicalCharacteristics Physical characteristics of the swerve drive from the JSON or + * built. + */ + public void configureSimModule( + org.ironmaple.simulation.drivesims.SwerveModuleSimulation simModule, + SwerveModulePhysicalCharacteristics physicalCharacteristics) { + this.mapleSimModule = + new SelfControlledSwerveDriveSimulation.SelfControlledModuleSimulation(simModule); + this.mapleSimModule.withCurrentLimits( + Amps.of(physicalCharacteristics.driveMotorCurrentLimit), + Amps.of(physicalCharacteristics.angleMotorCurrentLimit)); + } + + /** + * Update the position and state of the module. Called from {@link + * swervelib.SwerveModule#setDesiredState} function when simulated. + * + * @param desiredState State the swerve module is set to. + */ + public void updateStateAndPosition(SwerveModuleState desiredState) { + mapleSimModule.runModuleState(desiredState); + } + + /** + * Runs a drive motor characterization on the sim module. This is called from {@link + * swervelib.SwerveDriveTest#runDriveMotorsCharacterizationOnSimModules(SwerveDrive, double, + * boolean)} to run sysId during simulation + * + * @param desiredFacing the desired facing of the module + * @param volts the voltage to run + */ + public void runDriveMotorCharacterization(Rotation2d desiredFacing, double volts) { + mapleSimModule.runDriveMotorCharacterization(desiredFacing, volts); + } + + /** + * Runs a drive motor characterization on the sim module. This method is called from {@link + * swervelib.SwerveDriveTest#runAngleMotorsCharacterizationOnSimModules(SwerveDrive, double)} to + * run sysId during simulation + * + * @param volts the voltage to run + */ + public void runAngleMotorCharacterization(double volts) { + mapleSimModule.runSteerMotorCharacterization(volts); + } + + /** + * Get the simulated swerve module position. + * + * @return {@link SwerveModulePosition} of the simulated module. + */ + public SwerveModulePosition getPosition() { + return mapleSimModule.getModulePosition(); + } + + /** + * Get the {@link SwerveModuleState} of the simulated module. + * + * @return {@link SwerveModuleState} of the simulated module. + */ + public SwerveModuleState getState() { + if (mapleSimModule == null) { + return new SwerveModuleState(); + } + SwerveModuleState state = mapleSimModule.getMeasuredState(); + state.angle = state.angle.minus(Rotation2d.kZero); + return state; + } +} diff --git a/src/main/java/swervelib/simulation/package-info.java b/src/main/java/swervelib/simulation/package-info.java new file mode 100644 index 00000000..908fc590 --- /dev/null +++ b/src/main/java/swervelib/simulation/package-info.java @@ -0,0 +1,2 @@ +/** Classes used to simulate the swerve drive. */ +package swervelib.simulation; diff --git a/src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java b/src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java new file mode 100644 index 00000000..a0b03e36 --- /dev/null +++ b/src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java @@ -0,0 +1,346 @@ +package swervelib.telemetry; + +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.networktables.DoubleArrayPublisher; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.StructArrayPublisher; +import edu.wpi.first.networktables.StructPublisher; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj.Timer; +import swervelib.SwerveDrive; + +/** + * Telemetry to describe the {@link swervelib.SwerveDrive} following frc-web-components. (Which + * follows AdvantageKit) + */ +public class SwerveDriveTelemetry { + + /** An {@link Alert} for if the CAN ID is greater than 40. */ + public static final Alert canIdWarning = + new Alert( + "JSON", + "CAN IDs greater than 40 can cause undefined behaviour, please use a CAN ID below 40!", + AlertType.kWarning); + /** An {@link Alert} for if there is an I2C lockup issue on the roboRIO. */ + public static final Alert i2cLockupWarning = + new Alert( + "IMU", + "I2C lockup issue detected on roboRIO. Check console for more information.", + AlertType.kWarning); + /** NavX serial comm issue. */ + public static final Alert serialCommsIssueWarning = + new Alert( + "IMU", + "Serial comms is interrupted with USB and other serial traffic and causes intermittent connected/disconnection issues. Please consider another protocol or be mindful of this.", + AlertType.kWarning); + /** Module counter publisher for NT4 */ + private static final DoublePublisher moduleCountPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/moduleCount") + .publish(); + /** Module measured states for Nt4 */ + private static final DoubleArrayPublisher measuredStatesArrayPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleArrayTopic("swerve/measuredStates") + .publish(); + /** Desired states for NT4 */ + private static final DoubleArrayPublisher desiredStatesArrayPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleArrayTopic("swerve/desiredStates") + .publish(); + /** Measured chassis speeds array publisher. */ + private static final DoubleArrayPublisher measuredChassisSpeedsArrayPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleArrayTopic("swerve/measuredChassisSpeeds") + .publish(); + /** Desired chassis speeds array publisher. */ + private static final DoubleArrayPublisher desiredChassisSpeedsArrayPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleArrayTopic("swerve/desiredChassisSpeeds") + .publish(); + /** Robot rotation publisher. */ + private static final DoublePublisher robotRotationPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/robotRotation") + .publish(); + /** Max angular velocity publisher. */ + private static final DoublePublisher maxAngularVelocityPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/maxAngularVelocity") + .publish(); + /** Struct publisher for AdvantageScope swerve widgets. */ + private static final StructArrayPublisher measuredStatesStruct = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStructArrayTopic("swerve/advantagescope/currentStates", SwerveModuleState.struct) + .publish(); + /** Struct publisher for AdvantageScope swerve widgets. */ + private static final StructArrayPublisher desiredStatesStruct = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStructArrayTopic("swerve/advantagescope/desiredStates", SwerveModuleState.struct) + .publish(); + /** Measured {@link ChassisSpeeds} for NT4 AdvantageScope swerve widgets. */ + private static final StructPublisher measuredChassisSpeedsStruct = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStructTopic("swerve/advantagescope/measuredChassisSpeeds", ChassisSpeeds.struct) + .publish(); + /** Desired {@link ChassisSpeeds} for NT4 AdvantageScope swerve widgets. */ + private static final StructPublisher desiredChassisSpeedsStruct = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStructTopic("swerve/advantagescope/desiredChassisSpeeds", ChassisSpeeds.struct) + .publish(); + /** Robot {@link Rotation2d} for AdvantageScope swerve widgets. */ + private static final StructPublisher robotRotationStruct = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStructTopic("swerve/advantagescope/robotRotation", Rotation2d.struct) + .publish(); + /** Wheel locations array publisher for NT4. */ + private static final DoubleArrayPublisher wheelLocationsArrayPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleArrayTopic("swerve/wheelLocation") + .publish(); + /** Max speed publisher for NT4. */ + private static final DoublePublisher maxSpeedPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/maxSpeed") + .publish(); + /** Rotation unit for NT4. */ + private static final StringPublisher rotationUnitPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStringTopic("swerve/rotationUnit") + .publish(); + /** Chassis width publisher */ + private static final DoublePublisher sizeLeftRightPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/sizeLeftRight") + .publish(); + /** Chassis Length publisher. */ + private static final DoublePublisher sizeFrontBackPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/sizeFrontBack") + .publish(); + /** Chassis direction widget publisher. */ + private static final StringPublisher forwardDirectionPublisher = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getStringTopic("swerve/forwardDirection") + .publish(); + /** Odometry cycle time, updated whenever {@link SwerveDrive#updateOdometry()} is called. */ + private static final DoublePublisher odomCycleTime = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/odomCycleMS") + .publish(); + /** + * Control cycle time, updated whenever {@link + * swervelib.SwerveModule#setDesiredState(SwerveModuleState, boolean, double)} is called for the + * last module. + */ + private static final DoublePublisher ctrlCycleTime = + NetworkTableInstance.getDefault() + .getTable("SmartDashboard") + .getDoubleTopic("swerve/controlCycleMS") + .publish(); + /** Odometry timer to track cycle times. */ + private static final Timer odomTimer = new Timer(); + /** Control timer to track cycle times. */ + private static final Timer ctrlTimer = new Timer(); + /** Measured swerve module states object. */ + public static SwerveModuleState[] measuredStatesObj = new SwerveModuleState[4]; + /** Desired swerve module states object */ + public static SwerveModuleState[] desiredStatesObj = new SwerveModuleState[4]; + /** + * The maximum achievable angular velocity of the robot. This is used to visualize the angular + * velocity from the chassis speeds properties. + */ + public static ChassisSpeeds measuredChassisSpeedsObj = new ChassisSpeeds(); + /** Describes the desired forward, sideways and angular velocity of the robot. */ + public static ChassisSpeeds desiredChassisSpeedsObj = new ChassisSpeeds(); + /** The robot's current rotation based on odometry or gyro readings */ + public static Rotation2d robotRotationObj = new Rotation2d(); + /** The current telemetry verbosity level. */ + public static TelemetryVerbosity verbosity = TelemetryVerbosity.MACHINE; + /** State of simulation of the Robot, used to optimize retrieval. */ + public static boolean isSimulation = RobotBase.isSimulation(); + /** The number of swerve modules */ + public static int moduleCount; + /** The Locations of the swerve drive wheels. */ + public static double[] wheelLocations; + /** + * An array of rotation and velocity values describing the measured state of each swerve module + */ + public static double[] measuredStates; + /** An array of rotation and velocity values describing the desired state of each swerve module */ + public static double[] desiredStates; + /** The robot's current rotation based on odometry or gyro readings */ + public static double robotRotation = 0; + /** The maximum achievable speed of the modules, used to adjust the size of the vectors. */ + public static double maxSpeed; + /** The units of the module rotations and robot rotation */ + public static String rotationUnit = "degrees"; + /** The distance between the left and right modules. */ + public static double sizeLeftRight; + /** The distance between the front and back modules. */ + public static double sizeFrontBack; + /** + * The direction the robot should be facing when the "Robot Rotation" is zero or blank. This + * option is often useful to align with odometry data or match videos. 'up', 'right', 'down' or + * 'left' + */ + public static String forwardDirection = "up"; + /** + * The maximum achievable angular velocity of the robot. This is used to visualize the angular + * velocity from the chassis speeds properties. + */ + public static double maxAngularVelocity; + /** + * The maximum achievable angular velocity of the robot. This is used to visualize the angular + * velocity from the chassis speeds properties. + */ + public static double[] measuredChassisSpeeds = new double[3]; + /** Describes the desired forward, sideways and angular velocity of the robot. */ + public static double[] desiredChassisSpeeds = new double[3]; + /** Update the telemetry settings that infrequently change. */ + public static boolean updateSettings = true; + + /** Start the ctrl timer to measure cycle time, independent of periodic loops. */ + public static void startCtrlCycle() { + if (ctrlTimer.isRunning()) { + ctrlTimer.reset(); + } else { + ctrlTimer.start(); + } + } + + /** Update the Control cycle time. */ + public static void endCtrlCycle() { + if (DriverStation.isTeleopEnabled() + || DriverStation.isAutonomousEnabled() + || DriverStation.isTestEnabled()) { + // 100ms per module on initialization is normal + ctrlCycleTime.set(ctrlTimer.get() * 1000); + } + ctrlTimer.reset(); + } + + /** + * Start the odom cycle timer to calculate how long each odom took. Independent of periodic loops. + */ + public static void startOdomCycle() { + if (odomTimer.isRunning()) { + + odomTimer.reset(); + } else { + odomTimer.start(); + } + } + + /** Update the odom cycle time. */ + public static void endOdomCycle() { + if (DriverStation.isTeleopEnabled() + || DriverStation.isAutonomousEnabled() + || DriverStation.isTestEnabled()) { + odomCycleTime.set(odomTimer.get() * 1000); + } + odomTimer.reset(); + } + + /** Update only the settings that infrequently or never change. */ + public static void updateSwerveTelemetrySettings() { + if (updateSettings) { + updateSettings = false; + wheelLocationsArrayPublisher.set(wheelLocations); + maxSpeedPublisher.set(maxSpeed); + rotationUnitPublisher.set(rotationUnit); + sizeLeftRightPublisher.set(sizeLeftRight); + sizeFrontBackPublisher.set(sizeFrontBack); + forwardDirectionPublisher.set(forwardDirection); + } + } + + /** Upload data to smartdashboard */ + public static void updateData() { + if (updateSettings) { + updateSwerveTelemetrySettings(); + } + measuredChassisSpeeds[0] = measuredChassisSpeedsObj.vxMetersPerSecond; + measuredChassisSpeeds[1] = measuredChassisSpeedsObj.vyMetersPerSecond; + measuredChassisSpeeds[2] = Math.toDegrees(measuredChassisSpeedsObj.omegaRadiansPerSecond); + + desiredChassisSpeeds[0] = desiredChassisSpeedsObj.vxMetersPerSecond; + desiredChassisSpeeds[1] = desiredChassisSpeedsObj.vyMetersPerSecond; + desiredChassisSpeeds[2] = Math.toDegrees(desiredChassisSpeedsObj.omegaRadiansPerSecond); + + robotRotation = robotRotationObj.getDegrees(); + + for (int i = 0; i < measuredStatesObj.length; i++) { + SwerveModuleState state = measuredStatesObj[i]; + if (state != null) { + measuredStates[i * 2] = state.angle.getDegrees(); + measuredStates[i * 2 + 1] = state.speedMetersPerSecond; + } + } + + for (int i = 0; i < desiredStatesObj.length; i++) { + SwerveModuleState state = desiredStatesObj[i]; + if (state != null) { + desiredStates[i * 2] = state.angle.getDegrees(); + desiredStates[i * 2 + 1] = state.speedMetersPerSecond; + } + } + + moduleCountPublisher.set(moduleCount); + measuredStatesArrayPublisher.set(measuredStates); + desiredStatesArrayPublisher.set(desiredStates); + robotRotationPublisher.set(robotRotation); + maxAngularVelocityPublisher.set(maxAngularVelocity); + + measuredChassisSpeedsArrayPublisher.set(measuredChassisSpeeds); + desiredChassisSpeedsArrayPublisher.set(desiredChassisSpeeds); + + desiredStatesStruct.set(desiredStatesObj); + measuredStatesStruct.set(measuredStatesObj); + desiredChassisSpeedsStruct.set(desiredChassisSpeedsObj); + measuredChassisSpeedsStruct.set(measuredChassisSpeedsObj); + robotRotationStruct.set(robotRotationObj); + } + + /** Verbosity of telemetry data sent back. */ + public enum TelemetryVerbosity { + /** No telemetry data is sent back. */ + NONE, + /** Low telemetry data, only post the robot position on the field. */ + LOW, + /** Medium telemetry data, swerve directory */ + INFO, + /** Info level + field info */ + POSE, + /** Full swerve drive data is sent back in both human and machine readable forms. */ + HIGH, + /** Only send the machine readable data related to swerve drive. */ + MACHINE + } +} diff --git a/src/main/java/swervelib/telemetry/package-info.java b/src/main/java/swervelib/telemetry/package-info.java new file mode 100644 index 00000000..2f47093d --- /dev/null +++ b/src/main/java/swervelib/telemetry/package-info.java @@ -0,0 +1,2 @@ +/** Telemetry package for sending data to NT4 or SmartDashboard. */ +package swervelib.telemetry; diff --git a/vendordeps/yagsl-2025.8.0.json b/vendordeps/yagsl-2025.8.0.json deleted file mode 100644 index dc91af6e..00000000 --- a/vendordeps/yagsl-2025.8.0.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "fileName": "yagsl-2025.8.0.json", - "name": "YAGSL", - "version": "2025.8.0", - "frcYear": "2025", - "uuid": "1ccce5a4-acd2-4d18-bca3-4b8047188400", - "mavenUrls": [ - "https://broncbotz3481.github.io/YAGSL-Lib/yagsl/repos" - ], - "jsonUrl": "https://broncbotz3481.github.io/YAGSL-Lib/yagsl/yagsl.json", - "javaDependencies": [ - { - "groupId": "swervelib", - "artifactId": "YAGSL-java", - "version": "2025.8.0" - } - ], - "requires": [ - { - "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", - "errorMessage": "REVLib is required!", - "offlineFileName": "REVLib-2025.0.0.json", - "onlineUrl": "https://software-metadata.revrobotics.com/REVLib-2025.json" - }, - { - "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", - "errorMessage": "ReduxLib is required!", - "offlineFileName": "ReduxLib-2025.0.0.json", - "onlineUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2025.json" - }, - { - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", - "errorMessage": "Phoenix6 is required!", - "offlineFileName": "Phoenix6-25.1.0.json", - "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2025-latest.json" - }, - { - "uuid": "ab676553-b602-441f-a38d-f1296eff6537", - "errorMessage": "Phoenix5 is required!", - "offlineFileName": "Phoenix5-5.35.0.json", - "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-frc2025-latest.json" - }, - { - "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", - "errorMessage": "Studica is required!", - "offlineFileName": "Studica-2025.0.0.json", - "onlineUrl": "https://dev.studica.com/releases/2025/Studica-2025.0.0.json" - }, - { - "uuid": "60b2694b-9e6e-4026-81ee-6f167946f4b0", - "errorMessage": "ThriftyLib is required!", - "offlineFileName": "ThriftyLib.json", - "onlineUrl": "https://docs.home.thethriftybot.com/ThriftyLib.json" - }, - { - "uuid": "c39481e8-4a63-4a4c-9df6-48d91e4da37b", - "errorMessage": "maple-sim is required for simulation", - "offlineFileName": "maple-sim.json", - "onlineUrl": "https://shenzhen-robotics-alliance.github.io/maple-sim/vendordep/maple-sim.json" - } - ], - "jniDependencies": [], - "cppDependencies": [] -} From 30dd9ed78f9c9897bf8e4af9383ce3f8d6102daa Mon Sep 17 00:00:00 2001 From: Curt Date: Sat, 8 Nov 2025 09:27:30 -0500 Subject: [PATCH 15/93] Update ThriftyLib APIs --- .../encoders/ThriftyNovaEncoderSwerve.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java index 5e1a80b6..9d1080f5 100644 --- a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java +++ b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java @@ -1,11 +1,11 @@ package swervelib.encoders; import com.thethriftybot.Conversion; -import com.thethriftybot.Conversion.PositionUnit; import com.thethriftybot.Conversion.VelocityUnit; import com.thethriftybot.ThriftyNova; import com.thethriftybot.ThriftyNova.EncoderType; import com.thethriftybot.ThriftyNova.ExternalEncoder; + import swervelib.motors.SwerveMotor; import swervelib.motors.ThriftyNovaSwerve; @@ -18,8 +18,6 @@ public class ThriftyNovaEncoderSwerve extends SwerveAbsoluteEncoder { protected boolean inverted = false; /** Offset of the absolute encoder. */ protected double offset = 0.0; - /** Position conversion object for the motor encoder */ - private Conversion positionConversion; /** Velocity conversion object for the motor encoder */ private Conversion velocityConversion; @@ -31,10 +29,10 @@ public class ThriftyNovaEncoderSwerve extends SwerveAbsoluteEncoder { */ public ThriftyNovaEncoderSwerve(SwerveMotor motor, String encoderType) { this.motor = (ThriftyNova) motor.getMotor(); - positionConversion = new Conversion(PositionUnit.DEGREES, EncoderType.ABS); velocityConversion = new Conversion(VelocityUnit.DEGREES_PER_SEC, EncoderType.ABS); this.motor.setExternalEncoder(ExternalEncoder.valueOf(encoderType)); - this.motor.useEncoderType(EncoderType.ABS); + setAbsoluteEncoderOffset(offset); + this.motor.setAbsoluteWrapping(true); } @Override @@ -69,9 +67,8 @@ public void configure(boolean inverted) { */ @Override public double getAbsolutePosition() { - double rawMotor = motor.getPosition(); - double convertedMotor = positionConversion.fromMotor(rawMotor); - return (convertedMotor + offset) * (inverted ? -1.0 : 1.0); + double rawMotor = motor.getPositionAbs(); + return rawMotor * (inverted ? -1.0 : 1.0); } /** Get the instantiated absolute encoder Object. */ @@ -89,6 +86,7 @@ public Object getAbsoluteEncoder() { @Override public boolean setAbsoluteEncoderOffset(double offset) { this.offset = offset; + motor.setAbsOffset((int)offset); return true; } From f3b5347a52a11968dc93cd8e71d2b8ec31a43d7e Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Wed, 12 Nov 2025 18:29:47 -0500 Subject: [PATCH 16/93] shooter config pls work --- simgui.json | 19 ++++---- .../yagsl_swerve/modules/backleft.json | 6 +-- .../yagsl_swerve/modules/backright.json | 6 +-- .../yagsl_swerve/modules/frontleft.json | 6 +-- .../yagsl_swerve/modules/frontright.json | 6 +-- .../yagsl_swerve/modules/pidfproperties.json | 10 ++-- src/main/java/frc/robot/BuildConstants.java | 10 ++-- src/main/java/frc/robot/RobotContainer.java | 15 +++++- .../frc/robot/blackteam/ShooterSubsystem.java | 47 +++++++++++++------ src/main/java/frc/robot/blackteam/feeder.java | 10 ++-- .../java/frc/robot/example/ExampleRobot.java | 2 +- .../frc/robot/example/ExampleSubsystem.java | 8 ++-- .../config/json/devices/ShooterParser.java | 4 +- .../devices/YamsShooterConfigurationJson.java | 12 ++--- .../encoders/ThriftyNovaEncoderSwerve.java | 3 +- vendordeps/yams.json | 4 +- 16 files changed, 98 insertions(+), 70 deletions(-) diff --git a/simgui.json b/simgui.json index a1b83a4c..e0eb3527 100644 --- a/simgui.json +++ b/simgui.json @@ -83,6 +83,10 @@ "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", "/SmartDashboard/Mechanisms/ShooterMech/FeederMotor/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/LiveTuning": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", @@ -116,11 +120,6 @@ "visible": true } }, - "/SmartDashboard/Arm/mechanism": { - "window": { - "visible": true - } - }, "/SmartDashboard/DrivePoseEstimator/values/Pose Field": { "CARPET0": { "image": ".\\pictures\\carpet.png", @@ -647,11 +646,6 @@ "visible": true } }, - "/SmartDashboard/Elevator/mechanism": { - "window": { - "visible": true - } - }, "/SmartDashboard/Field": { "OdometryPose": { "arrowColor": [ @@ -687,6 +681,11 @@ "visible": true } }, + "/SmartDashboard/ShooterMech/mechanism": { + "window": { + "visible": true + } + }, "/SmartDashboard/VisionSystemSim-main/Sim Field": { "bottom": 599, "builtin": "", diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 8cf34515..7c8d8bed 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 3, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 4, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index 750a2c7b..ec87696a 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 5, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 6, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index 5406ad95..98040dcc 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 1, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 2, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index 068f6d64..a380b67f 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 7, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 8, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json index e09a7131..5924fdbb 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json @@ -1,16 +1,16 @@ { "drive": { - "p": 0.00001, - "i": 0, - "d": 0, + "p": 0.00023, + "i": 0.0000002, + "d": 1, "f": 0, "iz": 0 }, "angle": { - "p": 0.0005, + "p": 0.0020645, "i": 0, "d": 0, - "f": 0, + "f": 0.001, "iz": 0 } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index c4f9fb82..89684706 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 42; - public static final String GIT_SHA = "b0d78855eb9f11e406324939938314ebe2e43a1a"; - public static final String GIT_DATE = "2025-10-20 18:51:17 EDT"; + public static final int GIT_REVISION = 63; + public static final String GIT_SHA = "78af47688f5c04d5cbc44f3d5bba9fa34d4d565f"; + public static final String GIT_DATE = "2025-11-10 18:06:59 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-10-22 17:49:00 EDT"; - public static final long BUILD_UNIX_TIME = 1761169740728L; + public static final String BUILD_DATE = "2025-11-12 18:24:01 EST"; + public static final long BUILD_UNIX_TIME = 1762989841946L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index 55c6b65c..b1f5bf41 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -4,7 +4,11 @@ package frc.robot; +import static edu.wpi.first.units.Units.RPM; + import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.button.CommandXboxController; +import frc.robot.blackteam.ShooterSubsystem; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.WpiHelperInterface; import org.frc5010.common.arch.WpiNetworkTableValuesHelper; @@ -15,19 +19,26 @@ public class RobotContainer implements WpiHelperInterface { private static final RobotsParser robotsParser = new RobotsParser(); public static Constants constants; private GenericRobot robot; + private final ShooterSubsystem shootersubsystem = new ShooterSubsystem(); + + private final CommandXboxController xboxcontroller = new CommandXboxController(0); public RobotContainer() { constants = new Constants(); robot = robotsParser.getRobot(); - + shootersubsystem.setDefaultCommand(shootersubsystem.set(0)); initAutoCommands(); configureButtonBindings(); WpiNetworkTableValuesHelper.loadRegisteredToNetworkTables(); } private void configureButtonBindings() { - robot.configureButtonBindings(); + xboxcontroller.a().whileTrue(shootersubsystem.setVelocity(RPM.of(60))); + xboxcontroller.b().whileTrue(shootersubsystem.setVelocity(RPM.of(300))); + + xboxcontroller.x().whileTrue(shootersubsystem.set(0.3)); + xboxcontroller.y().whileTrue(shootersubsystem.set(-0.3)); } // Just sets up defalt commands (setUpDeftCom) diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 8dd8d3c2..e44abd6e 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -7,11 +7,9 @@ import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; import static edu.wpi.first.units.Units.Seconds; -import static yams.mechanisms.SmartMechanism.gearbox; -import static yams.mechanisms.SmartMechanism.gearing; import com.thethriftybot.ThriftyNova; -import edu.wpi.first.math.controller.ArmFeedforward; +import edu.wpi.first.math.controller.SimpleMotorFeedforward; import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.units.measure.AngularVelocity; @@ -21,8 +19,9 @@ import java.util.function.DoubleSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; -import yams.mechanisms.config.ShooterConfig; -import yams.mechanisms.velocity.Shooter; +import yams.mechanisms.SmartMechanism; +import yams.mechanisms.config.FlyWheelConfig; +import yams.mechanisms.velocity.FlyWheel; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; @@ -42,28 +41,36 @@ public class ShooterSubsystem extends GenericSubsystem { private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController( - 4, 0, 0, DegreesPerSecond.of(180), DegreesPerSecondPerSecond.of(90)) - .withGearing(gearing(gearbox(3, 4))) + 50, 0, 0, DegreesPerSecond.of(90), DegreesPerSecondPerSecond.of(45)) + .withGearing(SmartMechanism.gearing(SmartMechanism.gearbox(3, 4))) .withIdleMode(MotorMode.COAST) .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) .withMotorInverted(false) + .withSimClosedLoopController( + 50, 0, 0, DegreesPerSecond.of(90), DegreesPerSecondPerSecond.of(45)) .withClosedLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new ArmFeedforward(0, 0, 0, 0)) + // .withOpenLoopRampRate(Seconds.of(0.25)) + .withSimFeedforward(new SimpleMotorFeedforward(0, 0)) + .withFeedforward(new SimpleMotorFeedforward(0, 0, 0)) .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); - /** Creates a new Shooter. */ - private final ShooterConfig shooterConfig = - new ShooterConfig(motorController) + + private final FlyWheelConfig shooterConfig = + new FlyWheelConfig(motorController) .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); + .withUpperSoftLimit(RPM.of(1000)) + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); - private final Shooter shooter = new Shooter(shooterConfig); + private FlyWheel shooter = new FlyWheel(shooterConfig); + /** + * @return Shooter velocity. + */ + /** Creates a new Shooter. */ public ShooterSubsystem() { distanceToVelocityMap.put(0.0, 0.0); distanceToVelocityMap.put(0.5, 500.0); @@ -73,6 +80,14 @@ public Command setSpeed(double speed) { return shooter.set(speed); } + /** + * @param dutyCycle DutyCycle to set. + * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} + */ + public Command set(double dutyCycle) { + return shooter.set(dutyCycle); + } + public Command launchToDistance(DoubleSupplier distanceSupplier) { return shooter.setSpeed( () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); @@ -99,4 +114,8 @@ public void periodic() { public void simulationPeriodic() { shooter.simIterate(); } + + public Command setVelocity(AngularVelocity speed) { + return shooter.setSpeed(speed); + } } diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index 3c5b540d..8c37cdb9 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -16,8 +16,8 @@ import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.wpilibj2.command.Command; import org.frc5010.common.arch.GenericSubsystem; -import yams.mechanisms.config.ShooterConfig; -import yams.mechanisms.velocity.Shooter; +import yams.mechanisms.config.FlyWheelConfig; +import yams.mechanisms.velocity.FlyWheel; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; @@ -41,14 +41,14 @@ public class feeder extends GenericSubsystem { private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); - private final ShooterConfig feederConfig = - new ShooterConfig(motorController) + private final FlyWheelConfig feederConfig = + new FlyWheelConfig(motorController) .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) .withUpperSoftLimit(RPM.of(5000)); - private final Shooter feeder = new Shooter(feederConfig); + private final FlyWheel feeder = new FlyWheel(feederConfig); public Command setSpeed(double speed) { return feeder.set(speed); diff --git a/src/main/java/frc/robot/example/ExampleRobot.java b/src/main/java/frc/robot/example/ExampleRobot.java index 05a535b0..01c3f14f 100644 --- a/src/main/java/frc/robot/example/ExampleRobot.java +++ b/src/main/java/frc/robot/example/ExampleRobot.java @@ -37,7 +37,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.createAButton().onTrue(exampleSubsystem.addBallToRobot()); driver.createBButton().onTrue(exampleSubsystem.launchBall()); driver.createXButton().whileTrue(exampleSubsystem.setDutyCycle(0.5)); - driver.createYButton().onTrue(exampleSubsystem.sysIdShooter()); + driver.createYButton().onTrue(exampleSubsystem.sysIdFlyWheel()); State idle = stateMachine.addState("idle", Commands.idle()); stateMachine.setInitialState(idle); diff --git a/src/main/java/frc/robot/example/ExampleSubsystem.java b/src/main/java/frc/robot/example/ExampleSubsystem.java index a54c4f60..755b87a9 100644 --- a/src/main/java/frc/robot/example/ExampleSubsystem.java +++ b/src/main/java/frc/robot/example/ExampleSubsystem.java @@ -38,7 +38,7 @@ import org.ironmaple.simulation.seasonspecific.crescendo2024.NoteOnFly; import org.ironmaple.simulation.seasonspecific.reefscape2025.ReefscapeAlgaeOnFly; import org.littletonrobotics.junction.Logger; -import yams.mechanisms.velocity.Shooter; +import yams.mechanisms.velocity.FlyWheel; public class ExampleSubsystem extends GenericSubsystem { protected PercentControlMotor motor; @@ -50,13 +50,13 @@ public class ExampleSubsystem extends GenericSubsystem { protected GamePieceProjectile gamePieceProjectile; protected int scoredNotes = 0; protected Rotation2d rotation = new Rotation2d(Degrees.of(180)); - protected Shooter shooter; + protected FlyWheel shooter; public ExampleSubsystem() { super("example.json"); this.motor = (PercentControlMotor) devices.get("percent_motor"); this.controlledMotor = (VelocityControlMotor) devices.get("velocity_motor"); - this.shooter = (Shooter) devices.get("Shooter"); + this.shooter = (FlyWheel) devices.get("FlyWheel"); this.angularMotor = angularControlledMotor(); // verticalMotor = verticalControlledMotor(); @@ -131,7 +131,7 @@ public Command setPercentControlMotorReference(DoubleSupplier reference) { this); } - public Command sysIdShooter() { + public Command sysIdFlyWheel() { return SystemIdentification.getSysIdFullCommand( SystemIdentification.rpmSysIdRoutine(shooter.getMotor(), logPrefix, this), 5, 3, 3); } diff --git a/src/main/java/org/frc5010/common/config/json/devices/ShooterParser.java b/src/main/java/org/frc5010/common/config/json/devices/ShooterParser.java index f15544d0..2f2bbf3a 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/ShooterParser.java +++ b/src/main/java/org/frc5010/common/config/json/devices/ShooterParser.java @@ -9,7 +9,7 @@ import edu.wpi.first.wpilibj2.command.SubsystemBase; import java.io.File; import java.io.IOException; -import yams.mechanisms.velocity.Shooter; +import yams.mechanisms.velocity.FlyWheel; /** Add your docs here. */ public class ShooterParser { @@ -21,7 +21,7 @@ public class ShooterParser { * @param system the system to add the device to * @return the configured pivot, or null if there was an error */ - public static Shooter parse(String subDirectory, String filename, SubsystemBase system) { + public static FlyWheel parse(String subDirectory, String filename, SubsystemBase system) { try { File directory = new File(Filesystem.getDeployDirectory(), subDirectory); DeviceConfigReader.checkDirectory(directory); diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java index c2f2dd13..a9c151ce 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java @@ -12,8 +12,8 @@ import org.frc5010.common.motors.GenericMotorController; import yams.gearing.GearBox; import yams.gearing.MechanismGearing; -import yams.mechanisms.config.ShooterConfig; -import yams.mechanisms.velocity.Shooter; +import yams.mechanisms.config.FlyWheelConfig; +import yams.mechanisms.velocity.FlyWheel; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; @@ -39,7 +39,7 @@ public class YamsShooterConfigurationJson implements DeviceConfiguration { * @return the configured shooter */ @Override - public Shooter configure(SubsystemBase deviceHandler) { + public FlyWheel configure(SubsystemBase deviceHandler) { GenericMotorController motor = DeviceConfigReader.getMotor( motorSetup.controllerType, motorSetup.motorType, motorSetup.canId); @@ -71,8 +71,8 @@ public Shooter configure(SubsystemBase deviceHandler) { motor.getMotorConfig().getMotorSimulationType(motorSetup.numberOfMotors)); SmartMotorController smartMotor = motor.getSmartMotorController(motorConfig); - ShooterConfig shooterConfig = - new ShooterConfig(smartMotor) + FlyWheelConfig shooterConfig = + new FlyWheelConfig(smartMotor) // .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) .withDiameter(UnitsParser.parseDistance(diameter)) .withMass(UnitsParser.parseMass(mass)) @@ -82,7 +82,7 @@ public Shooter configure(SubsystemBase deviceHandler) { if (0 != moi) { shooterConfig.withMOI(moi); } - Shooter shooter = new Shooter(shooterConfig); + FlyWheel shooter = new FlyWheel(shooterConfig); return shooter; } } diff --git a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java index 9d1080f5..aec8edef 100644 --- a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java +++ b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java @@ -5,7 +5,6 @@ import com.thethriftybot.ThriftyNova; import com.thethriftybot.ThriftyNova.EncoderType; import com.thethriftybot.ThriftyNova.ExternalEncoder; - import swervelib.motors.SwerveMotor; import swervelib.motors.ThriftyNovaSwerve; @@ -86,7 +85,7 @@ public Object getAbsoluteEncoder() { @Override public boolean setAbsoluteEncoderOffset(double offset) { this.offset = offset; - motor.setAbsOffset((int)offset); + motor.setAbsOffset((int) offset); return true; } diff --git a/vendordeps/yams.json b/vendordeps/yams.json index 7dcbb420..3339b3fb 100644 --- a/vendordeps/yams.json +++ b/vendordeps/yams.json @@ -1,7 +1,7 @@ { "fileName": "yams.json", "name": "Yet Another Mechanism System", - "version": "2025.10.23.1", + "version": "2025.11.10", "frcYear": "2025", "uuid": "a1051e86-a979-4880-a28b-a0d5362d1d96", "mavenUrls": [ @@ -12,7 +12,7 @@ { "groupId": "yams", "artifactId": "YAMS-java", - "version": "2025.10.23.1" + "version": "2025.11.10" } ], "cppDependencies": [], From a173f36d342264a3151b276faeabf4662d59fcea Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Wed, 12 Nov 2025 18:56:43 -0500 Subject: [PATCH 17/93] Config updates --- .../black_robot/yagsl_swerve/modules/backleft.json | 1 + .../black_robot/yagsl_swerve/modules/backright.json | 1 + .../black_robot/yagsl_swerve/modules/frontleft.json | 1 + .../black_robot/yagsl_swerve/modules/frontright.json | 1 + src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 2 +- .../swervelib/encoders/ThriftyNovaEncoderSwerve.java | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 7c8d8bed..46883621 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -19,6 +19,7 @@ "angle": true }, "absoluteEncoderInverted": true, + "absoluteEncoderOffset": -304.189453, "location": { "front": -9.2813, "left": 9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index ec87696a..30225f1f 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -19,6 +19,7 @@ "angle": true }, "absoluteEncoderInverted": true, + "absoluteEncoderOffset": -221.396484, "location": { "front": -9.2813, "left": -9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index 98040dcc..873d67fe 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -19,6 +19,7 @@ "angle": true }, "absoluteEncoderInverted": true, + "absoluteEncoderOffset": -97.470703, "location": { "front": 9.2813, "left": 9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index a380b67f..dc9813d6 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -19,6 +19,7 @@ "angle": true }, "absoluteEncoderInverted": true, + "absoluteEncoderOffset": -175.253906, "location": { "front": 9.2813, "left": -9.2813 diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 89684706..e3f6abc7 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 63; - public static final String GIT_SHA = "78af47688f5c04d5cbc44f3d5bba9fa34d4d565f"; - public static final String GIT_DATE = "2025-11-10 18:06:59 EST"; + public static final int GIT_REVISION = 64; + public static final String GIT_SHA = "f3b5347a52a11968dc93cd8e71d2b8ec31a43d7e"; + public static final String GIT_DATE = "2025-11-12 18:29:47 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-12 18:24:01 EST"; - public static final long BUILD_UNIX_TIME = 1762989841946L; + public static final String BUILD_DATE = "2025-11-12 18:40:13 EST"; + public static final long BUILD_UNIX_TIME = 1762990813639L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 58f9fc01..405e6a8f 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -14,7 +14,7 @@ public class BlackRobot extends GenericRobot { public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); - shooterSubsystem = new ShooterSubsystem(); + // shooterSubsystem = new ShooterSubsystem(); // NamedCommands.registerCommand("shoot", launchToDistance(20)); } diff --git a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java index aec8edef..2fb31e7a 100644 --- a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java +++ b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java @@ -66,7 +66,7 @@ public void configure(boolean inverted) { */ @Override public double getAbsolutePosition() { - double rawMotor = motor.getPositionAbs(); + double rawMotor = motor.getPositionAbs() * (360.0 / 4096); return rawMotor * (inverted ? -1.0 : 1.0); } From ac5588d326d5093eabc7c5c0ff4f6b63d4e6e077 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Sat, 15 Nov 2025 12:15:51 -0500 Subject: [PATCH 18/93] Working Black YAGSL values --- src/main/deploy/black_robot/yagsl_drivetrain.json | 7 +------ .../black_robot/yagsl_swerve/modules/backleft.json | 4 ++-- .../black_robot/yagsl_swerve/modules/backright.json | 4 ++-- .../black_robot/yagsl_swerve/modules/frontleft.json | 4 ++-- .../black_robot/yagsl_swerve/modules/frontright.json | 4 ++-- .../yagsl_swerve/modules/pidfproperties.json | 10 +++++----- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/main/deploy/black_robot/yagsl_drivetrain.json b/src/main/deploy/black_robot/yagsl_drivetrain.json index d99f723b..6609776c 100644 --- a/src/main/deploy/black_robot/yagsl_drivetrain.json +++ b/src/main/deploy/black_robot/yagsl_drivetrain.json @@ -1,12 +1,7 @@ { "directory": "black_robot/yagsl_swerve", "turningMotorGearRatio": 1.0, - "driveModules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ], + "driveModules": [], "startingPose": { "x": { "val": 1.0, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 46883621..560ed689 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -16,10 +16,10 @@ }, "inverted": { "drive": true, - "angle": true + "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": -304.189453, + "absoluteEncoderOffset": 0, "location": { "front": -9.2813, "left": 9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index 30225f1f..6afb711e 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -16,10 +16,10 @@ }, "inverted": { "drive": true, - "angle": true + "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": -221.396484, + "absoluteEncoderOffset": 0, "location": { "front": -9.2813, "left": -9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index 873d67fe..df764070 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -16,10 +16,10 @@ }, "inverted": { "drive": true, - "angle": true + "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": -97.470703, + "absoluteEncoderOffset": 0, "location": { "front": 9.2813, "left": 9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index dc9813d6..c578241b 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -16,10 +16,10 @@ }, "inverted": { "drive": true, - "angle": true + "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": -175.253906, + "absoluteEncoderOffset": 0, "location": { "front": 9.2813, "left": -9.2813 diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json index 5924fdbb..bf95c1ca 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json @@ -1,16 +1,16 @@ { "drive": { - "p": 0.00023, - "i": 0.0000002, - "d": 1, + "p": 0.0020645, + "i": 0, + "d": 0, "f": 0, "iz": 0 }, "angle": { - "p": 0.0020645, + "p": 0.01, "i": 0, "d": 0, - "f": 0.001, + "f": 0.0, "iz": 0 } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index e3f6abc7..235eec49 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 64; - public static final String GIT_SHA = "f3b5347a52a11968dc93cd8e71d2b8ec31a43d7e"; - public static final String GIT_DATE = "2025-11-12 18:29:47 EST"; + public static final int GIT_REVISION = 65; + public static final String GIT_SHA = "a173f36d342264a3151b276faeabf4662d59fcea"; + public static final String GIT_DATE = "2025-11-12 18:56:43 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-12 18:40:13 EST"; - public static final long BUILD_UNIX_TIME = 1762990813639L; + public static final String BUILD_DATE = "2025-11-15 11:38:54 EST"; + public static final long BUILD_UNIX_TIME = 1763224734062L; public static final int DIRTY = 1; private BuildConstants() {} From 680763447bb53db3266897e7c94ac31dc86d4bd0 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Sat, 15 Nov 2025 12:35:04 -0500 Subject: [PATCH 19/93] Re-enable shooter --- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 2 +- .../java/frc/robot/blackteam/ShooterSubsystem.java | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 235eec49..fcf67625 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 65; - public static final String GIT_SHA = "a173f36d342264a3151b276faeabf4662d59fcea"; - public static final String GIT_DATE = "2025-11-12 18:56:43 EST"; + public static final int GIT_REVISION = 66; + public static final String GIT_SHA = "ac5588d326d5093eabc7c5c0ff4f6b63d4e6e077"; + public static final String GIT_DATE = "2025-11-15 12:15:51 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-15 11:38:54 EST"; - public static final long BUILD_UNIX_TIME = 1763224734062L; + public static final String BUILD_DATE = "2025-11-15 12:33:23 EST"; + public static final long BUILD_UNIX_TIME = 1763228003879L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 405e6a8f..58f9fc01 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -14,7 +14,7 @@ public class BlackRobot extends GenericRobot { public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); - // shooterSubsystem = new ShooterSubsystem(); + shooterSubsystem = new ShooterSubsystem(); // NamedCommands.registerCommand("shoot", launchToDistance(20)); } diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index e44abd6e..422a91f0 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -63,6 +63,7 @@ public class ShooterSubsystem extends GenericSubsystem { .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) .withUpperSoftLimit(RPM.of(1000)) + .withSpeedometerSimulation() .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); private FlyWheel shooter = new FlyWheel(shooterConfig); From e01a09fa87ce7ebb9ca4bee31f428393f5e93c51 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Mon, 17 Nov 2025 16:29:28 -0500 Subject: [PATCH 20/93] ,nm,nm, --- .../yagsl_swerve/modules/pidfproperties.json | 2 +- src/main/java/frc/robot/RobotContainer.java | 9 +----- .../java/frc/robot/blackteam/BlackRobot.java | 28 +++++++++++++++---- .../java/frc/robot/example/ExampleRobot.java | 10 ++++--- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json index a0d528dc..735cb6b4 100644 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json @@ -7,7 +7,7 @@ "iz": 0 }, "angle": { - "p": 0.01125, + "p": 0.5, "i": 0.000002, "d": 0, "f": 0, diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index b1f5bf41..e4dbb372 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -29,17 +29,10 @@ public RobotContainer() { robot = robotsParser.getRobot(); shootersubsystem.setDefaultCommand(shootersubsystem.set(0)); initAutoCommands(); - configureButtonBindings(); + WpiNetworkTableValuesHelper.loadRegisteredToNetworkTables(); } - private void configureButtonBindings() { - xboxcontroller.a().whileTrue(shootersubsystem.setVelocity(RPM.of(60))); - xboxcontroller.b().whileTrue(shootersubsystem.setVelocity(RPM.of(300))); - - xboxcontroller.x().whileTrue(shootersubsystem.set(0.3)); - xboxcontroller.y().whileTrue(shootersubsystem.set(-0.3)); - } // Just sets up defalt commands (setUpDeftCom) public void setupDefaults() { diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 58f9fc01..b34899cb 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -1,12 +1,17 @@ package frc.robot.blackteam; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; +import static edu.wpi.first.units.Units.RPM; + import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.config.ConfigConstants; import org.frc5010.common.drive.GenericDrivetrain; import org.frc5010.common.sensors.Controller; +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.CommandXboxController; + public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; private ShooterSubsystem shooterSubsystem; @@ -22,11 +27,16 @@ private Command launchToDistance(int i) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); } - + ShooterSubsystem shootersubsystem = new ShooterSubsystem(); + private final CommandXboxController xboxcontroller = new CommandXboxController(0); @Override - public void configureButtonBindings(Controller driver, Controller operator) { - driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); - } + public void configureButtonBindings() { + xboxcontroller.leftTrigger().whileTrue(shootersubsystem.setVelocity(RPM.of(60))); + xboxcontroller.rightTrigger().whileTrue(shootersubsystem.setVelocity(RPM.of(300))); + + xboxcontroller.x().whileTrue(shootersubsystem.set(0.3)); + xboxcontroller.y().whileTrue(shootersubsystem.set(-0.3)); + } @Override public void setupDefaultCommands(Controller driver, Controller operator) { @@ -49,4 +59,10 @@ public void buildAutoCommands() { super.buildAutoCommands(); selectableCommand.addOption("Do Nothing", Commands.none()); } + + @Override + public void configureButtonBindings(Controller driver, Controller operator) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'configureButtonBindings'"); + } } diff --git a/src/main/java/frc/robot/example/ExampleRobot.java b/src/main/java/frc/robot/example/ExampleRobot.java index 01c3f14f..44caa6c9 100644 --- a/src/main/java/frc/robot/example/ExampleRobot.java +++ b/src/main/java/frc/robot/example/ExampleRobot.java @@ -5,9 +5,8 @@ package frc.robot.example; import static edu.wpi.first.units.Units.RPM; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; +import yams.mechanisms.config.FlyWheelConfig; +import yams.mechanisms.velocity.FlyWheel; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.StateMachine; import org.frc5010.common.arch.StateMachine.State; @@ -17,6 +16,9 @@ import org.frc5010.common.motors.function.PercentControlMotor; import org.frc5010.common.sensors.Controller; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; + /** This is an example robot class. */ public class ExampleRobot extends GenericRobot { SwerveConstants swerveConstants; @@ -37,7 +39,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.createAButton().onTrue(exampleSubsystem.addBallToRobot()); driver.createBButton().onTrue(exampleSubsystem.launchBall()); driver.createXButton().whileTrue(exampleSubsystem.setDutyCycle(0.5)); - driver.createYButton().onTrue(exampleSubsystem.sysIdFlyWheel()); + driver.createYButton().onTrue(exampleSubsystem.sysIdShooter()); State idle = stateMachine.addState("idle", Commands.idle()); stateMachine.setInitialState(idle); From 67c5a1a89073d72ced90966607178de23f4b6ab9 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Mon, 17 Nov 2025 18:38:24 -0500 Subject: [PATCH 21/93] fadc --- .SysId/sysid-window.json | 47 +++++++++++++++++++ .SysId/sysid.json | 1 + simgui.json | 20 ++++++-- src/main/java/frc/robot/BuildConstants.java | 10 ++-- src/main/java/frc/robot/RobotContainer.java | 14 ++---- .../java/frc/robot/blackteam/BlackRobot.java | 35 ++++++-------- .../frc/robot/blackteam/ShooterSubsystem.java | 20 +++++--- .../java/frc/robot/example/ExampleRobot.java | 8 ++-- 8 files changed, 104 insertions(+), 51 deletions(-) create mode 100644 .SysId/sysid-window.json create mode 100644 .SysId/sysid.json diff --git a/.SysId/sysid-window.json b/.SysId/sysid-window.json new file mode 100644 index 00000000..a14c7462 --- /dev/null +++ b/.SysId/sysid-window.json @@ -0,0 +1,47 @@ +{ + "MainWindow": { + "GLOBAL": { + "font": "Proggy Dotted", + "fps": "120", + "height": "720", + "maximized": "0", + "style": "0", + "userScale": "2", + "width": "1280", + "xpos": "-1", + "ypos": "-1" + } + }, + "Window": { + "###Analyzer": { + "Collapsed": "0", + "Pos": "320,25", + "Size": "360,550" + }, + "###Data Selector": { + "Collapsed": "0", + "Pos": "5,480", + "Size": "310,235" + }, + "###Log Loader": { + "Collapsed": "0", + "Pos": "5,25", + "Size": "310,450" + }, + "###Program Log": { + "Collapsed": "0", + "Pos": "320,580", + "Size": "360,135" + }, + "Debug##Default": { + "Collapsed": "0", + "Pos": "60,60", + "Size": "400,400" + }, + "Diagnostic Plots": { + "Collapsed": "0", + "Pos": "685,25", + "Size": "590,690" + } + } +} diff --git a/.SysId/sysid.json b/.SysId/sysid.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.SysId/sysid.json @@ -0,0 +1 @@ +{} diff --git a/simgui.json b/simgui.json index bcbf64ea..f5c41022 100644 --- a/simgui.json +++ b/simgui.json @@ -94,6 +94,7 @@ "/SmartDashboard/Alerts": "Alerts", "/SmartDashboard/Arm/mechanism": "Mechanism2d", "/SmartDashboard/Auto Modes": "String Chooser", + "/SmartDashboard/BlackRobot/Auto Modes": "String Chooser", "/SmartDashboard/Drive Visual": "Mechanism2d", "/SmartDashboard/DrivePoseEstimator/Pose Field": "Field2d", "/SmartDashboard/Elevator/mechanism": "Mechanism2d", @@ -107,14 +108,20 @@ "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/Up": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", "/SmartDashboard/Pigeon 2 (v6) [13]": "Gyro", + "/SmartDashboard/Pigeon 2 (v6) [9]": "Gyro", "/SmartDashboard/Pigeon 2 [13]": "Gyro", "/SmartDashboard/Robot Visual": "Mechanism2d", "/SmartDashboard/SendableChooser[0]": "String Chooser", "/SmartDashboard/Shooter/mechanism": "Mechanism2d", + "/SmartDashboard/ShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/Swerve Drive": "Alerts", "/SmartDashboard/Turret/mechanism": "Mechanism2d", "/SmartDashboard/VisionSystemSim-Vision/Sim Field": "Field2d", @@ -128,7 +135,12 @@ "visible": true } }, - "/SmartDashboard/Arm/mechanism": { + "/SmartDashboard/BlackRobot/Auto Modes": { + "window": { + "visible": true + } + }, + "/SmartDashboard/Drive Visual": { "window": { "visible": true } @@ -694,14 +706,12 @@ "left": 534, "right": 3466, "top": 291, - "width": 17.54825210571289 - }, - "/SmartDashboard/Robot Visual": { + "width": 17.54825210571289, "window": { "visible": true } }, - "/SmartDashboard/Shooter/mechanism": { + "/SmartDashboard/ShooterMech/mechanism": { "window": { "visible": true } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index fcf67625..24d48513 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 66; - public static final String GIT_SHA = "ac5588d326d5093eabc7c5c0ff4f6b63d4e6e077"; - public static final String GIT_DATE = "2025-11-15 12:15:51 EST"; + public static final int GIT_REVISION = 72; + public static final String GIT_SHA = "e01a09fa87ce7ebb9ca4bee31f428393f5e93c51"; + public static final String GIT_DATE = "2025-11-17 16:29:28 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-15 12:33:23 EST"; - public static final long BUILD_UNIX_TIME = 1763228003879L; + public static final String BUILD_DATE = "2025-11-17 18:34:45 EST"; + public static final long BUILD_UNIX_TIME = 1763422485070L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java index e4dbb372..55c6b65c 100644 --- a/src/main/java/frc/robot/RobotContainer.java +++ b/src/main/java/frc/robot/RobotContainer.java @@ -4,11 +4,7 @@ package frc.robot; -import static edu.wpi.first.units.Units.RPM; - import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.button.CommandXboxController; -import frc.robot.blackteam.ShooterSubsystem; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.WpiHelperInterface; import org.frc5010.common.arch.WpiNetworkTableValuesHelper; @@ -19,20 +15,20 @@ public class RobotContainer implements WpiHelperInterface { private static final RobotsParser robotsParser = new RobotsParser(); public static Constants constants; private GenericRobot robot; - private final ShooterSubsystem shootersubsystem = new ShooterSubsystem(); - - private final CommandXboxController xboxcontroller = new CommandXboxController(0); public RobotContainer() { constants = new Constants(); robot = robotsParser.getRobot(); - shootersubsystem.setDefaultCommand(shootersubsystem.set(0)); + initAutoCommands(); - + configureButtonBindings(); WpiNetworkTableValuesHelper.loadRegisteredToNetworkTables(); } + private void configureButtonBindings() { + robot.configureButtonBindings(); + } // Just sets up defalt commands (setUpDeftCom) public void setupDefaults() { diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index b34899cb..a44c1c2e 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -2,16 +2,13 @@ import static edu.wpi.first.units.Units.RPM; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.config.ConfigConstants; import org.frc5010.common.drive.GenericDrivetrain; import org.frc5010.common.sensors.Controller; -import edu.wpi.first.wpilibj.XboxController; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.CommandXboxController; - public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; private ShooterSubsystem shooterSubsystem; @@ -27,16 +24,20 @@ private Command launchToDistance(int i) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); } - ShooterSubsystem shootersubsystem = new ShooterSubsystem(); - private final CommandXboxController xboxcontroller = new CommandXboxController(0); + @Override - public void configureButtonBindings() { - xboxcontroller.leftTrigger().whileTrue(shootersubsystem.setVelocity(RPM.of(60))); - xboxcontroller.rightTrigger().whileTrue(shootersubsystem.setVelocity(RPM.of(300))); - - xboxcontroller.x().whileTrue(shootersubsystem.set(0.3)); - xboxcontroller.y().whileTrue(shootersubsystem.set(-0.3)); - } + public void configureButtonBindings(Controller driver, Controller operator) { + + driver.createRightBumper(); + driver.createLeftBumper(); + driver.LEFT_BUMPER.whileTrue(shooterSubsystem.setVelocity(RPM.of(60))); + driver.RIGHT_BUMPER.whileTrue(shooterSubsystem.setVelocity(RPM.of(300))); + driver.createXButton(); + driver.createYButton(); + driver.X_BUTTON.whileTrue(shooterSubsystem.set(0.3)); + driver.Y_BUTTON.whileTrue(shooterSubsystem.set(-0.3)); + driver.createAButton().whileTrue(shooterSubsystem.systemID()); + } @Override public void setupDefaultCommands(Controller driver, Controller operator) { @@ -59,10 +60,4 @@ public void buildAutoCommands() { super.buildAutoCommands(); selectableCommand.addOption("Do Nothing", Commands.none()); } - - @Override - public void configureButtonBindings(Controller driver, Controller operator) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'configureButtonBindings'"); - } } diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 422a91f0..0f08ad2b 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -1,12 +1,13 @@ package frc.robot.blackteam; import static edu.wpi.first.units.Units.Amps; -import static edu.wpi.first.units.Units.DegreesPerSecond; -import static edu.wpi.first.units.Units.DegreesPerSecondPerSecond; import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; +import static edu.wpi.first.units.Units.RotationsPerSecondPerSecond; +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 com.thethriftybot.ThriftyNova; import edu.wpi.first.math.controller.SimpleMotorFeedforward; @@ -41,18 +42,18 @@ public class ShooterSubsystem extends GenericSubsystem { private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController( - 50, 0, 0, DegreesPerSecond.of(90), DegreesPerSecondPerSecond.of(45)) + 0.00024509, 0, 0, RPM.of(1000), RotationsPerSecondPerSecond.of(500)) .withGearing(SmartMechanism.gearing(SmartMechanism.gearbox(3, 4))) - .withIdleMode(MotorMode.COAST) + .withIdleMode(MotorMode.BRAKE) .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) .withMotorInverted(false) .withSimClosedLoopController( - 50, 0, 0, DegreesPerSecond.of(90), DegreesPerSecondPerSecond.of(45)) + 0.00024509, 0, 0, RPM.of(1000), RotationsPerSecondPerSecond.of(500)) .withClosedLoopRampRate(Seconds.of(0.25)) // .withOpenLoopRampRate(Seconds.of(0.25)) - .withSimFeedforward(new SimpleMotorFeedforward(0, 0)) - .withFeedforward(new SimpleMotorFeedforward(0, 0, 0)) + .withSimFeedforward(new SimpleMotorFeedforward(0, 0.12521, 0.43498)) + .withFeedforward(new SimpleMotorFeedforward(0, 0.12521, 0.43498)) .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorController motorController = @@ -63,6 +64,7 @@ public class ShooterSubsystem extends GenericSubsystem { .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) .withUpperSoftLimit(RPM.of(1000)) + .withLowerSoftLimit(RPM.of(-1000)) .withSpeedometerSimulation() .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); @@ -119,4 +121,8 @@ public void simulationPeriodic() { public Command setVelocity(AngularVelocity speed) { return shooter.setSpeed(speed); } + + public Command systemID() { + return shooter.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); + } } diff --git a/src/main/java/frc/robot/example/ExampleRobot.java b/src/main/java/frc/robot/example/ExampleRobot.java index 44caa6c9..05a535b0 100644 --- a/src/main/java/frc/robot/example/ExampleRobot.java +++ b/src/main/java/frc/robot/example/ExampleRobot.java @@ -5,8 +5,9 @@ package frc.robot.example; import static edu.wpi.first.units.Units.RPM; -import yams.mechanisms.config.FlyWheelConfig; -import yams.mechanisms.velocity.FlyWheel; + +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.StateMachine; import org.frc5010.common.arch.StateMachine.State; @@ -16,9 +17,6 @@ import org.frc5010.common.motors.function.PercentControlMotor; import org.frc5010.common.sensors.Controller; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; - /** This is an example robot class. */ public class ExampleRobot extends GenericRobot { SwerveConstants swerveConstants; From eb9d8aaa87df98c3843f41ab24d777a03ef34607 Mon Sep 17 00:00:00 2001 From: Curt Date: Mon, 17 Nov 2025 21:27:07 -0500 Subject: [PATCH 22/93] Refactor shooter and feeder gearing configuration Updated ShooterSubsystem and feeder to use MechanismGearing and GearBox for motor gearing configuration instead of SmartMechanism. Adjusted PID and feedforward parameters in ShooterSubsystem for improved control. Fixed telemetry label in feeder configuration. --- src/main/java/frc/robot/BuildConstants.java | 10 +++---- .../frc/robot/blackteam/ShooterSubsystem.java | 28 +++++++++---------- src/main/java/frc/robot/blackteam/feeder.java | 8 +++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 24d48513..0aa3b3b5 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 72; - public static final String GIT_SHA = "e01a09fa87ce7ebb9ca4bee31f428393f5e93c51"; - public static final String GIT_DATE = "2025-11-17 16:29:28 EST"; + public static final int GIT_REVISION = 73; + public static final String GIT_SHA = "67c5a1a89073d72ced90966607178de23f4b6ab9"; + public static final String GIT_DATE = "2025-11-17 18:38:24 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-17 18:34:45 EST"; - public static final long BUILD_UNIX_TIME = 1763422485070L; + public static final String BUILD_DATE = "2025-11-17 21:25:10 EST"; + public static final long BUILD_UNIX_TIME = 1763432710661L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 0f08ad2b..5ce3b94a 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -20,7 +20,8 @@ import java.util.function.DoubleSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; -import yams.mechanisms.SmartMechanism; +import yams.gearing.GearBox; +import yams.gearing.MechanismGearing; import yams.mechanisms.config.FlyWheelConfig; import yams.mechanisms.velocity.FlyWheel; import yams.motorcontrollers.SmartMotorController; @@ -32,6 +33,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(10); + // private final SparkMax motor = new SparkMax(10, MotorType.kBrushless); private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( Map.entry(0.0, 0.0), @@ -42,30 +44,31 @@ public class ShooterSubsystem extends GenericSubsystem { private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController( - 0.00024509, 0, 0, RPM.of(1000), RotationsPerSecondPerSecond.of(500)) - .withGearing(SmartMechanism.gearing(SmartMechanism.gearbox(3, 4))) + 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) + .withSimClosedLoopController( + 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) + .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) .withIdleMode(MotorMode.BRAKE) .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) .withMotorInverted(false) - .withSimClosedLoopController( - 0.00024509, 0, 0, RPM.of(1000), RotationsPerSecondPerSecond.of(500)) .withClosedLoopRampRate(Seconds.of(0.25)) + // ThriftyNova does not support separate closed loop and open loop ramp rates // .withOpenLoopRampRate(Seconds.of(0.25)) - .withSimFeedforward(new SimpleMotorFeedforward(0, 0.12521, 0.43498)) - .withFeedforward(new SimpleMotorFeedforward(0, 0.12521, 0.43498)) + .withFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) + .withSimFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + // new SparkWrapper(motor, DCMotor.getNEO(1), motorConfig); private final FlyWheelConfig shooterConfig = new FlyWheelConfig(motorController) .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) - .withUpperSoftLimit(RPM.of(1000)) - .withLowerSoftLimit(RPM.of(-1000)) - .withSpeedometerSimulation() + .withSoftLimit(RPM.of(-500), RPM.of(500)) + .withSpeedometerSimulation(RPM.of(750)) .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); private FlyWheel shooter = new FlyWheel(shooterConfig); @@ -74,10 +77,7 @@ public class ShooterSubsystem extends GenericSubsystem { */ /** Creates a new Shooter. */ - public ShooterSubsystem() { - distanceToVelocityMap.put(0.0, 0.0); - distanceToVelocityMap.put(0.5, 500.0); - } + public ShooterSubsystem() {} public Command setSpeed(double speed) { return shooter.set(speed); diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index 8c37cdb9..758114eb 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -9,13 +9,13 @@ import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; import static edu.wpi.first.units.Units.Seconds; -import static yams.mechanisms.SmartMechanism.gearbox; -import static yams.mechanisms.SmartMechanism.gearing; import com.thethriftybot.ThriftyNova; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.wpilibj2.command.Command; import org.frc5010.common.arch.GenericSubsystem; +import yams.gearing.GearBox; +import yams.gearing.MechanismGearing; import yams.mechanisms.config.FlyWheelConfig; import yams.mechanisms.velocity.FlyWheel; import yams.motorcontrollers.SmartMotorController; @@ -30,7 +30,7 @@ public class feeder extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(11); private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) - .withGearing(gearing(gearbox(3, 4))) + .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) .withIdleMode(MotorMode.BRAKE) .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) @@ -45,7 +45,7 @@ public class feeder extends GenericSubsystem { new FlyWheelConfig(motorController) .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH) + .withTelemetry("FeederMech", TelemetryVerbosity.HIGH) .withUpperSoftLimit(RPM.of(5000)); private final FlyWheel feeder = new FlyWheel(feederConfig); From 1b060cd9575fdc9c4927af85ac5ec24f721ae490 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Thu, 20 Nov 2025 16:12:10 -0500 Subject: [PATCH 23/93] renamed everything except telemetry dont really know how. --- src/main/java/frc/robot/BuildConstants.java | 10 ++--- .../frc/robot/blackteam/ShooterSubsystem.java | 39 ++++++++++++------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 24d48513..43c5e466 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 72; - public static final String GIT_SHA = "e01a09fa87ce7ebb9ca4bee31f428393f5e93c51"; - public static final String GIT_DATE = "2025-11-17 16:29:28 EST"; + public static final int GIT_REVISION = 73; + public static final String GIT_SHA = "67c5a1a89073d72ced90966607178de23f4b6ab9"; + public static final String GIT_DATE = "2025-11-17 18:38:24 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-17 18:34:45 EST"; - public static final long BUILD_UNIX_TIME = 1763422485070L; + public static final String BUILD_DATE = "2025-11-19 17:55:01 EST"; + public static final long BUILD_UNIX_TIME = 1763592901693L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 0f08ad2b..64d83ef5 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -39,6 +39,7 @@ public class ShooterSubsystem extends GenericSubsystem { Map.entry(1.0, 1000.0), Map.entry(1.5, 1500.0)); + @SuppressWarnings("removal") private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController( @@ -59,7 +60,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); - private final FlyWheelConfig shooterConfig = + private final FlyWheelConfig lFlyWheelConfig = new FlyWheelConfig(motorController) .withDiameter(Inches.of(4)) .withMass(Pounds.of(1)) @@ -68,11 +69,19 @@ public class ShooterSubsystem extends GenericSubsystem { .withSpeedometerSimulation() .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); - private FlyWheel shooter = new FlyWheel(shooterConfig); - /** - * @return Shooter velocity. - */ + private FlyWheel lowerFlyWheel = new FlyWheel(lFlyWheelConfig); + + private final FlyWheelConfig uFlyWheelConfig = + new FlyWheelConfig(motorController) + .withDiameter(Inches.of(4)) + .withMass(Pounds.of(1)) + .withUpperSoftLimit(RPM.of(1000)) + .withLowerSoftLimit(RPM.of(-1000)) + .withSpeedometerSimulation() + .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); + + private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); /** Creates a new Shooter. */ public ShooterSubsystem() { distanceToVelocityMap.put(0.0, 0.0); @@ -80,7 +89,7 @@ public ShooterSubsystem() { } public Command setSpeed(double speed) { - return shooter.set(speed); + return lowerFlyWheel.set(speed); } /** @@ -88,41 +97,41 @@ public Command setSpeed(double speed) { * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} */ public Command set(double dutyCycle) { - return shooter.set(dutyCycle); + return lowerFlyWheel.set(dutyCycle); } public Command launchToDistance(DoubleSupplier distanceSupplier) { - return shooter.setSpeed( + return lowerFlyWheel.setSpeed( () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); } public Command spinAtSpeed(DoubleSupplier speedSupplier) { - return shooter.setSpeed(RPM.of(speedSupplier.getAsDouble())); + return lowerFlyWheel.setSpeed(RPM.of(speedSupplier.getAsDouble())); } public Supplier getVelocity() { - return () -> shooter.getSpeed(); + return () -> lowerFlyWheel.getSpeed(); } public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range) { - return shooter.isNear(expected, range); + return lowerFlyWheel.isNear(expected, range); } @Override public void periodic() { - shooter.updateTelemetry(); + lowerFlyWheel.updateTelemetry(); } @Override public void simulationPeriodic() { - shooter.simIterate(); + lowerFlyWheel.simIterate(); } public Command setVelocity(AngularVelocity speed) { - return shooter.setSpeed(speed); + return lowerFlyWheel.setSpeed(speed); } public Command systemID() { - return shooter.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); + return lowerFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); } } From 4d982c6f25a90c5c5f7332ab73beaf5cab9ffece Mon Sep 17 00:00:00 2001 From: Adamtheboss9 Date: Thu, 20 Nov 2025 17:02:29 -0500 Subject: [PATCH 24/93] Added States --- simgui.json | 5 ++--- src/main/java/frc/robot/BuildConstants.java | 10 ++++----- .../java/frc/robot/blackteam/BlackRobot.java | 21 +++++++++++++++++++ src/main/java/frc/robot/blackteam/feeder.java | 5 +++-- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/simgui.json b/simgui.json index f5c41022..3a64d72d 100644 --- a/simgui.json +++ b/simgui.json @@ -90,6 +90,7 @@ "/LiveWindow/Ungrouped/Pigeon 2 [13]": "Gyro", "/LiveWindow/Ungrouped/Scheduler": "Scheduler", "/Shuffleboard/BabySwerve/Auto Modes": "String Chooser", + "/Shuffleboard/BlackRobot/Auto Modes": "String Chooser", "/Shuffleboard/ExampleRobot/Auto Modes": "String Chooser", "/SmartDashboard/Alerts": "Alerts", "/SmartDashboard/Arm/mechanism": "Mechanism2d", @@ -97,6 +98,7 @@ "/SmartDashboard/BlackRobot/Auto Modes": "String Chooser", "/SmartDashboard/Drive Visual": "Mechanism2d", "/SmartDashboard/DrivePoseEstimator/Pose Field": "Field2d", + "/SmartDashboard/DrivePoseEstimator/values/Pose Field": "Field2d", "/SmartDashboard/Elevator/mechanism": "Mechanism2d", "/SmartDashboard/Encoders": "Alerts", "/SmartDashboard/ExampleRobot/Auto Modes": "String Chooser", @@ -804,8 +806,5 @@ } } } - }, - "NetworkTables Info": { - "visible": true } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 24d48513..11dfac07 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 72; - public static final String GIT_SHA = "e01a09fa87ce7ebb9ca4bee31f428393f5e93c51"; - public static final String GIT_DATE = "2025-11-17 16:29:28 EST"; + public static final int GIT_REVISION = 73; + public static final String GIT_SHA = "67c5a1a89073d72ced90966607178de23f4b6ab9"; + public static final String GIT_DATE = "2025-11-17 18:38:24 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-17 18:34:45 EST"; - public static final long BUILD_UNIX_TIME = 1763422485070L; + public static final String BUILD_DATE = "2025-11-20 17:01:41 EST"; + public static final long BUILD_UNIX_TIME = 1763676101907L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index a44c1c2e..0df14ff3 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -4,7 +4,10 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.JoystickButton; import org.frc5010.common.arch.GenericRobot; +import org.frc5010.common.arch.StateMachine; +import org.frc5010.common.arch.StateMachine.State; import org.frc5010.common.config.ConfigConstants; import org.frc5010.common.drive.GenericDrivetrain; import org.frc5010.common.sensors.Controller; @@ -12,11 +15,13 @@ public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; private ShooterSubsystem shooterSubsystem; + private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); shooterSubsystem = new ShooterSubsystem(); + // NamedCommands.registerCommand("shoot", launchToDistance(20)); } @@ -37,6 +42,22 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.X_BUTTON.whileTrue(shooterSubsystem.set(0.3)); driver.Y_BUTTON.whileTrue(shooterSubsystem.set(-0.3)); driver.createAButton().whileTrue(shooterSubsystem.systemID()); + driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); + + JoystickButton rightBumper = driver.createRightBumper(); + + State idle = + flyWheelStateMachine.addState("idle", Commands.print("IDLE").andThen(Commands.idle())); + State prep = + flyWheelStateMachine.addState("prep", Commands.print("PREP").andThen(Commands.idle())); + State fire = + flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(Commands.idle())); + + flyWheelStateMachine.setInitialState(idle); + idle.switchTo(prep).when(rightBumper); + prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(3000), RPM.of(200))); + prep.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); + fire.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); } @Override diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index 8c37cdb9..38d7021d 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -26,8 +26,9 @@ import yams.motorcontrollers.local.NovaWrapper; /** Add your docs here. */ -public class feeder extends GenericSubsystem { +public class Feeder extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(11); + private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withGearing(gearing(gearbox(3, 4))) @@ -54,7 +55,7 @@ public Command setSpeed(double speed) { return feeder.set(speed); } - public feeder() {} + public Feeder() {} @Override public void periodic() { From ad3457b6a0cfcc3c3d7fd62ff8c3d1be8f46b04e Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Thu, 20 Nov 2025 17:13:40 -0500 Subject: [PATCH 25/93] srfvs --- .../java/frc/robot/blackteam/ShooterSubsystem.java | 10 +++++----- src/main/java/frc/robot/blackteam/feeder.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 9cc0f630..493fef58 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -50,7 +50,7 @@ public class ShooterSubsystem extends GenericSubsystem { 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) .withIdleMode(MotorMode.BRAKE) - .withTelemetry("ShooterMotor", TelemetryVerbosity.HIGH) + .withTelemetry("LowerShooterMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) .withMotorInverted(false) .withClosedLoopRampRate(Seconds.of(0.25)) @@ -66,7 +66,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final FlyWheelConfig lFlyWheelConfig = new FlyWheelConfig(motorController) - .withDiameter(Inches.of(4)) + .withDiameter(Inches.of(3)) .withMass(Pounds.of(1)) .withSoftLimit(RPM.of(-500), RPM.of(500)) .withSpeedometerSimulation(RPM.of(750)) @@ -77,12 +77,12 @@ public class ShooterSubsystem extends GenericSubsystem { private final FlyWheelConfig uFlyWheelConfig = new FlyWheelConfig(motorController) - .withDiameter(Inches.of(4)) + .withDiameter(Inches.of(3)) .withMass(Pounds.of(1)) - .withUpperSoftLimit(RPM.of(1000)) + .withUpperSoftLimit(RPM.of(100000)) .withLowerSoftLimit(RPM.of(-1000)) .withSpeedometerSimulation() - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); + .withTelemetry("UpperShooterMech", TelemetryVerbosity.HIGH); private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); /** Creates a new Shooter. */ diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index 6c6f3b46..4ed178e4 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -26,7 +26,7 @@ import yams.motorcontrollers.local.NovaWrapper; /** Add your docs here. */ -public class Feeder extends GenericSubsystem { +public class feeder extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(11); private final SmartMotorControllerConfig motorConfig = @@ -44,7 +44,7 @@ public class Feeder extends GenericSubsystem { private final FlyWheelConfig feederConfig = new FlyWheelConfig(motorController) - .withDiameter(Inches.of(4)) + .withDiameter(Inches.of(3)) .withMass(Pounds.of(1)) .withTelemetry("FeederMech", TelemetryVerbosity.HIGH) .withUpperSoftLimit(RPM.of(5000)); @@ -55,7 +55,7 @@ public Command setSpeed(double speed) { return feeder.set(speed); } - public Feeder() {} + public feeder() {} @Override public void periodic() { From f8a88b3e07c9a911a4fcd2c74fc0e235da2fa6a6 Mon Sep 17 00:00:00 2001 From: Adamtheboss9 Date: Thu, 20 Nov 2025 17:53:52 -0500 Subject: [PATCH 26/93] Parial to do list, shooter changes --- simgui.json | 23 +++++++++--------- src/main/java/frc/robot/BuildConstants.java | 10 ++++---- .../java/frc/robot/blackteam/BlackRobot.java | 10 ++++---- .../frc/robot/blackteam/ShooterSubsystem.java | 24 ++++++++++++------- src/main/java/frc/robot/blackteam/feeder.java | 4 ++-- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/simgui.json b/simgui.json index 3a64d72d..a8324105 100644 --- a/simgui.json +++ b/simgui.json @@ -110,6 +110,10 @@ "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/Up": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/LowerShooterMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/LowerShooterMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/LowerShooterMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/ShooterMech/LowerShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Down": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Up": "Command", @@ -126,6 +130,7 @@ "/SmartDashboard/ShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/Swerve Drive": "Alerts", "/SmartDashboard/Turret/mechanism": "Mechanism2d", + "/SmartDashboard/UpperShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/VisionSystemSim-Vision/Sim Field": "Field2d", "/SmartDashboard/VisionSystemSim-main/Sim Field": "Field2d", "/SmartDashboard/navX-Sensor[1]": "Gyro", @@ -668,10 +673,7 @@ "height": 6.0, "image": ".\\pictures\\LobbinLoco.png", "right": 1432, - "width": 14.600000381469727, - "window": { - "visible": true - } + "width": 14.600000381469727 }, "/SmartDashboard/Elevator/mechanism": { "window": { @@ -708,10 +710,7 @@ "left": 534, "right": 3466, "top": 291, - "width": 17.54825210571289, - "window": { - "visible": true - } + "width": 17.54825210571289 }, "/SmartDashboard/ShooterMech/mechanism": { "window": { @@ -724,10 +723,7 @@ "height": 6.0, "image": ".\\pictures\\LobbinLoco.png", "right": 1432, - "width": 14.600000381469727, - "window": { - "visible": true - } + "width": 14.600000381469727 } } }, @@ -806,5 +802,8 @@ } } } + }, + "NetworkTables View": { + "visible": false } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 11dfac07..c12c6bf5 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 73; - public static final String GIT_SHA = "67c5a1a89073d72ced90966607178de23f4b6ab9"; - public static final String GIT_DATE = "2025-11-17 18:38:24 EST"; + public static final int GIT_REVISION = 79; + public static final String GIT_SHA = "ad3457b6a0cfcc3c3d7fd62ff8c3d1be8f46b04e"; + public static final String GIT_DATE = "2025-11-20 17:13:40 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-20 17:01:41 EST"; - public static final long BUILD_UNIX_TIME = 1763676101907L; + public static final String BUILD_DATE = "2025-11-20 17:19:52 EST"; + public static final long BUILD_UNIX_TIME = 1763677192311L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 0df14ff3..9c0b0bf9 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -42,9 +42,9 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.X_BUTTON.whileTrue(shooterSubsystem.set(0.3)); driver.Y_BUTTON.whileTrue(shooterSubsystem.set(-0.3)); driver.createAButton().whileTrue(shooterSubsystem.systemID()); - driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); + driver.createAButton().onTrue(shooterSubsystem.setLowerFLyWheelSpeed(0.5)); - JoystickButton rightBumper = driver.createRightBumper(); + JoystickButton bButton = driver.createBButton(); State idle = flyWheelStateMachine.addState("idle", Commands.print("IDLE").andThen(Commands.idle())); @@ -54,10 +54,10 @@ public void configureButtonBindings(Controller driver, Controller operator) { flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(Commands.idle())); flyWheelStateMachine.setInitialState(idle); - idle.switchTo(prep).when(rightBumper); + idle.switchTo(prep).when(bButton); prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(3000), RPM.of(200))); - prep.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); - fire.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); + prep.switchTo(idle).when(() -> !bButton.getAsBoolean()); + fire.switchTo(idle).when(() -> !bButton.getAsBoolean()); } @Override diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 493fef58..91a08517 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -32,7 +32,8 @@ import yams.motorcontrollers.local.NovaWrapper; public class ShooterSubsystem extends GenericSubsystem { - private final ThriftyNova motor = new ThriftyNova(10); + private final ThriftyNova lowerMotor = new ThriftyNova(10); + private final ThriftyNova upperMotor = new ThriftyNova(11); // private final SparkMax motor = new SparkMax(10, MotorType.kBrushless); private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( @@ -60,12 +61,14 @@ public class ShooterSubsystem extends GenericSubsystem { .withSimFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) .withControlMode(ControlMode.CLOSED_LOOP); - private final SmartMotorController motorController = - new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + private final SmartMotorController lowerMotorController = + new NovaWrapper(lowerMotor, DCMotor.getNEO(1), motorConfig); // new SparkWrapper(motor, DCMotor.getNEO(1), motorConfig); + private final SmartMotorController upperMotorController = + new NovaWrapper(upperMotor, DCMotor.getNEO(1), motorConfig); private final FlyWheelConfig lFlyWheelConfig = - new FlyWheelConfig(motorController) + new FlyWheelConfig(lowerMotorController) .withDiameter(Inches.of(3)) .withMass(Pounds.of(1)) .withSoftLimit(RPM.of(-500), RPM.of(500)) @@ -74,24 +77,27 @@ public class ShooterSubsystem extends GenericSubsystem { private FlyWheel lowerFlyWheel = new FlyWheel(lFlyWheelConfig); - private final FlyWheelConfig uFlyWheelConfig = - new FlyWheelConfig(motorController) + new FlyWheelConfig(lowerMotorController) .withDiameter(Inches.of(3)) .withMass(Pounds.of(1)) .withUpperSoftLimit(RPM.of(100000)) .withLowerSoftLimit(RPM.of(-1000)) .withSpeedometerSimulation() .withTelemetry("UpperShooterMech", TelemetryVerbosity.HIGH); - - private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); + + private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); /** Creates a new Shooter. */ public ShooterSubsystem() {} - public Command setSpeed(double speed) { + public Command setLowerFLyWheelSpeed(double speed) { return lowerFlyWheel.set(speed); } + public Command setUpperFLyWheelSpeed(double speed) { + return upperFlyWheel.set(speed); + } + /** * @param dutyCycle DutyCycle to set. * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index 4ed178e4..12a0fd45 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -26,7 +26,7 @@ import yams.motorcontrollers.local.NovaWrapper; /** Add your docs here. */ -public class feeder extends GenericSubsystem { +public class Feeder extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(11); private final SmartMotorControllerConfig motorConfig = @@ -55,7 +55,7 @@ public Command setSpeed(double speed) { return feeder.set(speed); } - public feeder() {} + public Feeder() {} @Override public void periodic() { From 530ed7bfbb3f52ebde2112fea0e92b573c73847e Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Thu, 20 Nov 2025 17:53:59 -0500 Subject: [PATCH 27/93] mass --- src/main/java/frc/robot/blackteam/ShooterSubsystem.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 493fef58..446bc9f8 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -41,7 +41,6 @@ public class ShooterSubsystem extends GenericSubsystem { Map.entry(1.0, 1000.0), Map.entry(1.5, 1500.0)); - @SuppressWarnings("removal") private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController( @@ -67,7 +66,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final FlyWheelConfig lFlyWheelConfig = new FlyWheelConfig(motorController) .withDiameter(Inches.of(3)) - .withMass(Pounds.of(1)) + .withMass(Pounds.of(2)) .withSoftLimit(RPM.of(-500), RPM.of(500)) .withSpeedometerSimulation(RPM.of(750)) .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); @@ -78,7 +77,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final FlyWheelConfig uFlyWheelConfig = new FlyWheelConfig(motorController) .withDiameter(Inches.of(3)) - .withMass(Pounds.of(1)) + .withMass(Pounds.of(2)) .withUpperSoftLimit(RPM.of(100000)) .withLowerSoftLimit(RPM.of(-1000)) .withSpeedometerSimulation() From b35f2b67522a272f880e0d1fc39290147ca20e11 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Sat, 22 Nov 2025 08:07:27 -0500 Subject: [PATCH 28/93] feeder --- src/main/java/frc/robot/blackteam/BlackRobot.java | 4 ++-- src/main/java/frc/robot/blackteam/feeder.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 0df14ff3..6e19a296 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -24,7 +24,7 @@ public BlackRobot(String directory) { // NamedCommands.registerCommand("shoot", launchToDistance(20)); } - + private Feeder feeder = new Feeder(); private Command launchToDistance(int i) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); @@ -42,7 +42,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.X_BUTTON.whileTrue(shooterSubsystem.set(0.3)); driver.Y_BUTTON.whileTrue(shooterSubsystem.set(-0.3)); driver.createAButton().whileTrue(shooterSubsystem.systemID()); - driver.createAButton().onTrue(shooterSubsystem.setSpeed(0.5)); + driver.createAButton().onTrue(feeder.setSpeed(0.5)); JoystickButton rightBumper = driver.createRightBumper(); diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/feeder.java index 4ed178e4..12a0fd45 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/feeder.java @@ -26,7 +26,7 @@ import yams.motorcontrollers.local.NovaWrapper; /** Add your docs here. */ -public class feeder extends GenericSubsystem { +public class Feeder extends GenericSubsystem { private final ThriftyNova motor = new ThriftyNova(11); private final SmartMotorControllerConfig motorConfig = @@ -55,7 +55,7 @@ public Command setSpeed(double speed) { return feeder.set(speed); } - public feeder() {} + public Feeder() {} @Override public void periodic() { From 5a0bb9d0c753b37e7268ed67969a286d301b680b Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Sat, 22 Nov 2025 08:11:49 -0500 Subject: [PATCH 29/93] black robot got deleted for me if it happened to anyone else let me know. this should be it though. --- .../java/frc/robot/blackteam/BlackRobot.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index e69de29b..6e19a296 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -0,0 +1,84 @@ +package frc.robot.blackteam; + +import static edu.wpi.first.units.Units.RPM; + +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.JoystickButton; +import org.frc5010.common.arch.GenericRobot; +import org.frc5010.common.arch.StateMachine; +import org.frc5010.common.arch.StateMachine.State; +import org.frc5010.common.config.ConfigConstants; +import org.frc5010.common.drive.GenericDrivetrain; +import org.frc5010.common.sensors.Controller; + +public class BlackRobot extends GenericRobot { + private GenericDrivetrain drivetrain; + private ShooterSubsystem shooterSubsystem; + private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); + + public BlackRobot(String directory) { + super(directory); + drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); + shooterSubsystem = new ShooterSubsystem(); + + // NamedCommands.registerCommand("shoot", launchToDistance(20)); + } + private Feeder feeder = new Feeder(); + private Command launchToDistance(int i) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); + } + + @Override + public void configureButtonBindings(Controller driver, Controller operator) { + + driver.createRightBumper(); + driver.createLeftBumper(); + driver.LEFT_BUMPER.whileTrue(shooterSubsystem.setVelocity(RPM.of(60))); + driver.RIGHT_BUMPER.whileTrue(shooterSubsystem.setVelocity(RPM.of(300))); + driver.createXButton(); + driver.createYButton(); + driver.X_BUTTON.whileTrue(shooterSubsystem.set(0.3)); + driver.Y_BUTTON.whileTrue(shooterSubsystem.set(-0.3)); + driver.createAButton().whileTrue(shooterSubsystem.systemID()); + driver.createAButton().onTrue(feeder.setSpeed(0.5)); + + JoystickButton rightBumper = driver.createRightBumper(); + + State idle = + flyWheelStateMachine.addState("idle", Commands.print("IDLE").andThen(Commands.idle())); + State prep = + flyWheelStateMachine.addState("prep", Commands.print("PREP").andThen(Commands.idle())); + State fire = + flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(Commands.idle())); + + flyWheelStateMachine.setInitialState(idle); + idle.switchTo(prep).when(rightBumper); + prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(3000), RPM.of(200))); + prep.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); + fire.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); + } + + @Override + public void setupDefaultCommands(Controller driver, Controller operator) { + driver.setRightTrigger(driver.createRightTrigger()); + drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); + } + + @Override + public void initAutoCommands() { + drivetrain.setAutoBuilder(); + } + + @Override + public Command generateAutoCommand(Command autoCommand) { + return drivetrain.generateAutoCommand(autoCommand); + } + + @Override + public void buildAutoCommands() { + super.buildAutoCommands(); + selectableCommand.addOption("Do Nothing", Commands.none()); + } +} From 7ce2e625067aaaa21675cd2eaaf23131cfd48a67 Mon Sep 17 00:00:00 2001 From: CameronSkea Date: Sat, 22 Nov 2025 08:20:12 -0500 Subject: [PATCH 30/93] config --- src/main/java/frc/robot/blackteam/BlackRobot.java | 3 ++- src/main/java/frc/robot/blackteam/ShooterSubsystem.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 6e19a296..95616de3 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -25,7 +25,8 @@ public BlackRobot(String directory) { // NamedCommands.registerCommand("shoot", launchToDistance(20)); } private Feeder feeder = new Feeder(); - private Command launchToDistance(int i) { + @SuppressWarnings("unused") +private Command launchToDistance(int i) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); } diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 0314d40b..f265b346 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -77,7 +77,7 @@ public class ShooterSubsystem extends GenericSubsystem { private FlyWheel lowerFlyWheel = new FlyWheel(lFlyWheelConfig); private final FlyWheelConfig uFlyWheelConfig = - new FlyWheelConfig(lowerMotorController) + new FlyWheelConfig(upperMotorController) .withDiameter(Inches.of(3)) .withMass(Pounds.of(2)) .withUpperSoftLimit(RPM.of(100000)) From f1c1c456421d757e9e310be7e45a0c21a6b4cb6d Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Sat, 22 Nov 2025 12:23:49 -0500 Subject: [PATCH 31/93] Fix sim crashes and configure commands and add 3d field --- simgui.json | 41 ++++++++--- .../akit_swerve_drivetrain.json | 3 + src/main/deploy/buttercup_swerve/cameras.json | 5 ++ .../buttercup_swerve/cameras/intake.json | 13 ++++ .../cameras/localization.json | 13 ++++ .../buttercup_swerve/cameras/quest.json | 11 +++ .../buttercup_swerve/cameras/shooter.json | 16 +++++ .../buttercup_swerve/competition_mode.json | 6 ++ .../deploy/buttercup_swerve/controllers.json | 5 ++ .../controllers/axis/driver_left_trigger.json | 8 +++ .../controllers/axis/driver_left_x.json | 8 +++ .../controllers/axis/driver_left_y.json | 8 +++ .../axis/driver_right_trigger.json | 8 +++ .../controllers/axis/driver_right_x.json | 8 +++ .../controllers/axis/operator_left_y.json | 9 +++ .../controllers/axis/operator_right_y.json | 9 +++ .../buttercup_swerve/controllers/driver.json | 11 +++ .../controllers/operator.json | 8 +++ .../deploy/buttercup_swerve/demo_mode.json | 6 ++ .../drive_modules/backleft.json | 5 ++ .../drive_modules/backright.json | 5 ++ .../drive_modules/frontleft.json | 5 ++ .../drive_modules/frontright.json | 5 ++ .../buttercup_swerve/field/game_pieces.json | 10 +++ src/main/deploy/buttercup_swerve/robot.json | 16 +++++ .../buttercup_swerve/subsystems/example.json | 7 ++ .../subsystems/example/percent_motor.json | 11 +++ .../subsystems/example/velocity_motor.json | 18 +++++ .../subsystems/example/yams_arm.json | 64 ++++++++++++++++++ .../subsystems/example/yams_elevator.json | 59 ++++++++++++++++ .../subsystems/example/yams_pivot.json | 57 ++++++++++++++++ .../buttercup_swerve/yagsl_drivetrain.json | 19 ++++++ .../yagsl_swerve/controllerproperties.json | 8 +++ .../yagsl_swerve/modules/backleft.json | 27 ++++++++ .../yagsl_swerve/modules/backright.json | 27 ++++++++ .../yagsl_swerve/modules/frontleft.json | 27 ++++++++ .../yagsl_swerve/modules/frontright.json | 27 ++++++++ .../modules/physicalproperties.json | 22 ++++++ .../yagsl_swerve/modules/pidfproperties.json | 16 +++++ .../yagsl_swerve/swervedrive.json | 14 ++++ src/main/deploy/robots.json | 8 ++- src/main/java/frc/robot/BuildConstants.java | 12 ++-- .../java/frc/robot/blackteam/BlackRobot.java | 28 ++++---- .../{feeder.java => FeederSubsystem.java} | 24 +++++-- .../frc/robot/blackteam/ShooterSubsystem.java | 11 ++- .../Field3d_LobbinLoco/LobbinLoco.glb | Bin 0 -> 393056 bytes .../userAssets/Field3d_LobbinLoco/config.json | 23 +++++++ .../userAssets/Robot_Black/config.json | 0 .../userAssets/Robot_Orange/config.json | 0 49 files changed, 712 insertions(+), 39 deletions(-) create mode 100644 src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json create mode 100644 src/main/deploy/buttercup_swerve/cameras.json create mode 100644 src/main/deploy/buttercup_swerve/cameras/intake.json create mode 100644 src/main/deploy/buttercup_swerve/cameras/localization.json create mode 100644 src/main/deploy/buttercup_swerve/cameras/quest.json create mode 100644 src/main/deploy/buttercup_swerve/cameras/shooter.json create mode 100644 src/main/deploy/buttercup_swerve/competition_mode.json create mode 100644 src/main/deploy/buttercup_swerve/controllers.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/driver.json create mode 100644 src/main/deploy/buttercup_swerve/controllers/operator.json create mode 100644 src/main/deploy/buttercup_swerve/demo_mode.json create mode 100644 src/main/deploy/buttercup_swerve/drive_modules/backleft.json create mode 100644 src/main/deploy/buttercup_swerve/drive_modules/backright.json create mode 100644 src/main/deploy/buttercup_swerve/drive_modules/frontleft.json create mode 100644 src/main/deploy/buttercup_swerve/drive_modules/frontright.json create mode 100644 src/main/deploy/buttercup_swerve/field/game_pieces.json create mode 100644 src/main/deploy/buttercup_swerve/robot.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/example.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_drivetrain.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json create mode 100644 src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json rename src/main/java/frc/robot/blackteam/{feeder.java => FeederSubsystem.java} (73%) create mode 100644 src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb delete mode 100644 src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Black/config.json delete mode 100644 src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Orange/config.json diff --git a/simgui.json b/simgui.json index a8324105..cc12c639 100644 --- a/simgui.json +++ b/simgui.json @@ -102,9 +102,14 @@ "/SmartDashboard/Elevator/mechanism": "Mechanism2d", "/SmartDashboard/Encoders": "Alerts", "/SmartDashboard/ExampleRobot/Auto Modes": "String Chooser", + "/SmartDashboard/FeederMech/mechanism": "Mechanism2d", "/SmartDashboard/Field": "Field2d", "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", + "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/Down": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/Up": "Command", @@ -118,6 +123,10 @@ "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/Up": "Command", "/SmartDashboard/Mechanisms/ShooterMech/ShooterMotor/Commands/ZeroEncoder": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", @@ -147,11 +156,6 @@ "visible": true } }, - "/SmartDashboard/Drive Visual": { - "window": { - "visible": true - } - }, "/SmartDashboard/DrivePoseEstimator/Pose Field": { "CARPET0": { "image": ".\\pictures\\carpet.png", @@ -673,9 +677,7 @@ "height": 6.0, "image": ".\\pictures\\LobbinLoco.png", "right": 1432, - "width": 14.600000381469727 - }, - "/SmartDashboard/Elevator/mechanism": { + "width": 14.600000381469727, "window": { "visible": true } @@ -685,6 +687,11 @@ "visible": true } }, + "/SmartDashboard/FeederMech/mechanism": { + "window": { + "visible": true + } + }, "/SmartDashboard/Field": { "OdometryPose": { "arrowColor": [ @@ -710,7 +717,15 @@ "left": 534, "right": 3466, "top": 291, - "width": 17.54825210571289 + "width": 17.54825210571289, + "window": { + "visible": true + } + }, + "/SmartDashboard/Mechanisms/ShooterMech/LowerShooterMotor/Commands/Down": { + "window": { + "visible": true + } }, "/SmartDashboard/ShooterMech/mechanism": { "window": { @@ -723,7 +738,10 @@ "height": 6.0, "image": ".\\pictures\\LobbinLoco.png", "right": 1432, - "width": 14.600000381469727 + "width": 14.600000381469727, + "window": { + "visible": true + } } } }, @@ -803,6 +821,9 @@ } } }, + "NetworkTables Info": { + "visible": true + }, "NetworkTables View": { "visible": false } diff --git a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json new file mode 100644 index 00000000..42f49455 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json @@ -0,0 +1,3 @@ +{ + "type": "SparkTalon" +} diff --git a/src/main/deploy/buttercup_swerve/cameras.json b/src/main/deploy/buttercup_swerve/cameras.json new file mode 100644 index 00000000..f308fd3e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/cameras.json @@ -0,0 +1,5 @@ +{ + "cameras": [], + "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", + "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" +} diff --git a/src/main/deploy/buttercup_swerve/cameras/intake.json b/src/main/deploy/buttercup_swerve/cameras/intake.json new file mode 100644 index 00000000..33938add --- /dev/null +++ b/src/main/deploy/buttercup_swerve/cameras/intake.json @@ -0,0 +1,13 @@ +{ + "name": "intake", + "use": "target", + "type": "limelight", + "column": 0, + "x": 0.15, + "y": 0.05, + "z": 0.2, + "roll": 0, + "pitch": 10, + "yaw": 0, + "targetHeight": 0.02 +} diff --git a/src/main/deploy/buttercup_swerve/cameras/localization.json b/src/main/deploy/buttercup_swerve/cameras/localization.json new file mode 100644 index 00000000..706b561a --- /dev/null +++ b/src/main/deploy/buttercup_swerve/cameras/localization.json @@ -0,0 +1,13 @@ +{ + "name": "localization", + "use": "apriltag", + "type": "limelight", + "strategy": "LOWEST_AMBIGUITY", + "column": 0, + "x": -0.35, + "y": -0.25, + "z": 0.5, + "roll": 0, + "pitch": -20, + "yaw": 180 +} diff --git a/src/main/deploy/buttercup_swerve/cameras/quest.json b/src/main/deploy/buttercup_swerve/cameras/quest.json new file mode 100644 index 00000000..df412311 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/cameras/quest.json @@ -0,0 +1,11 @@ +{ + "name": "quest", + "use": "quest", + "column": 0, + "x": 0, + "y": 0, + "z": 18, + "roll": 0, + "pitch": 0, + "yaw": 0 +} diff --git a/src/main/deploy/buttercup_swerve/cameras/shooter.json b/src/main/deploy/buttercup_swerve/cameras/shooter.json new file mode 100644 index 00000000..b60f68bc --- /dev/null +++ b/src/main/deploy/buttercup_swerve/cameras/shooter.json @@ -0,0 +1,16 @@ +{ + "name": "shooter", + "use": "target", + "type": "photonvision", + "column": 0, + "x": -0.25, + "y": 0, + "z": 0.25, + "roll": 0, + "pitch": 20, + "yaw": 180, + "targetFiducialIds": [ + 4, + 11 + ] +} diff --git a/src/main/deploy/buttercup_swerve/competition_mode.json b/src/main/deploy/buttercup_swerve/competition_mode.json new file mode 100644 index 00000000..f67b949b --- /dev/null +++ b/src/main/deploy/buttercup_swerve/competition_mode.json @@ -0,0 +1,6 @@ +{ + "maxSpeed": 6.0, + "maxAngularSpeed": 6.0, + "maxAccelleration": 3.0, + "maxAngularAccelleration": 18.75 +} diff --git a/src/main/deploy/buttercup_swerve/controllers.json b/src/main/deploy/buttercup_swerve/controllers.json new file mode 100644 index 00000000..901f9478 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers.json @@ -0,0 +1,5 @@ +{ + "controllers": [ + "driver.json" + ] +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json new file mode 100644 index 00000000..41b79133 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json @@ -0,0 +1,8 @@ +{ + "channel": 2, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json new file mode 100644 index 00000000..d77f0e91 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json @@ -0,0 +1,8 @@ +{ + "channel": 0, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json new file mode 100644 index 00000000..d826731e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_y.json @@ -0,0 +1,8 @@ +{ + "channel": 1, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json new file mode 100644 index 00000000..47b46917 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json @@ -0,0 +1,8 @@ +{ + "channel": 3, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json new file mode 100644 index 00000000..8f981e23 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json @@ -0,0 +1,8 @@ +{ + "channel": 4, + "deadband": 0.07, + "invert": true, + "scale": 1.0, + "curvePower": 3, + "limit": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json b/src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json new file mode 100644 index 00000000..c13bd0e0 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/operator_left_y.json @@ -0,0 +1,9 @@ +{ + "channel": 1, + "deadband": 0.07, + "invert": "true", + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0, + "rate": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json b/src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json new file mode 100644 index 00000000..2668956f --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/axis/operator_right_y.json @@ -0,0 +1,9 @@ +{ + "channel": 5, + "deadband": 0.07, + "invert": "true", + "scale": 1.0, + "curvePower": 3.0, + "limit": 1.0, + "rate": 1.0 +} diff --git a/src/main/deploy/buttercup_swerve/controllers/driver.json b/src/main/deploy/buttercup_swerve/controllers/driver.json new file mode 100644 index 00000000..e2dbc76a --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/driver.json @@ -0,0 +1,11 @@ +{ + "name": "driver", + "port": 0, + "axis": [ + "driver_left_y.json", + "driver_left_x.json", + "driver_right_x.json", + "driver_left_trigger.json", + "driver_right_trigger.json" + ] +} diff --git a/src/main/deploy/buttercup_swerve/controllers/operator.json b/src/main/deploy/buttercup_swerve/controllers/operator.json new file mode 100644 index 00000000..858cfea8 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/controllers/operator.json @@ -0,0 +1,8 @@ +{ + "name": "operator", + "port": 1, + "axis": [ + "operator_left_y.json", + "operator_right_y.json" + ] +} diff --git a/src/main/deploy/buttercup_swerve/demo_mode.json b/src/main/deploy/buttercup_swerve/demo_mode.json new file mode 100644 index 00000000..f60f73c9 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/demo_mode.json @@ -0,0 +1,6 @@ +{ + "maxSpeed": 2.0, + "maxAngularSpeed": 2.0, + "maxAccelleration": 1.0, + "maxAngularAccelleration": 9.0 +} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/backleft.json b/src/main/deploy/buttercup_swerve/drive_modules/backleft.json new file mode 100644 index 00000000..dd530f9e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/drive_modules/backleft.json @@ -0,0 +1,5 @@ +{ + "s": 0.0, + "v": 0.0, + "a": 0.0 +} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/backright.json b/src/main/deploy/buttercup_swerve/drive_modules/backright.json new file mode 100644 index 00000000..dd530f9e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/drive_modules/backright.json @@ -0,0 +1,5 @@ +{ + "s": 0.0, + "v": 0.0, + "a": 0.0 +} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/frontleft.json b/src/main/deploy/buttercup_swerve/drive_modules/frontleft.json new file mode 100644 index 00000000..dd530f9e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/drive_modules/frontleft.json @@ -0,0 +1,5 @@ +{ + "s": 0.0, + "v": 0.0, + "a": 0.0 +} diff --git a/src/main/deploy/buttercup_swerve/drive_modules/frontright.json b/src/main/deploy/buttercup_swerve/drive_modules/frontright.json new file mode 100644 index 00000000..dd530f9e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/drive_modules/frontright.json @@ -0,0 +1,5 @@ +{ + "s": 0.0, + "v": 0.0, + "a": 0.0 +} diff --git a/src/main/deploy/buttercup_swerve/field/game_pieces.json b/src/main/deploy/buttercup_swerve/field/game_pieces.json new file mode 100644 index 00000000..b13b37c1 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/field/game_pieces.json @@ -0,0 +1,10 @@ +{ + "gamePieces": [ + { + "x": 2, + "y": 7, + "type": "Coral", + "rotation": 0 + } + ] +} diff --git a/src/main/deploy/buttercup_swerve/robot.json b/src/main/deploy/buttercup_swerve/robot.json new file mode 100644 index 00000000..a2abafde --- /dev/null +++ b/src/main/deploy/buttercup_swerve/robot.json @@ -0,0 +1,16 @@ +{ + "userConfig": "competition_mode.json", + "driveType": "YAGSL_SWERVE_DRIVE", + "trackWidth": 18.5625, + "trackWidthUom": "in", + "wheelBase": 18.5625, + "wheelBaseUom": "in", + "wheelDiameter": 4, + "wheelDiameterUom": "in", + "physicalMaxSpeed": 5.93, + "physicalMaxSpeedUom": "m/s", + "driveMotorGearRatio": 1.0, + "loadSimulatedField": false, + "gamePieceA": "FRC5010Ball", + "gamePieceB": "FRC5010Ball" +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example.json b/src/main/deploy/buttercup_swerve/subsystems/example.json new file mode 100644 index 00000000..928061d8 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/example.json @@ -0,0 +1,7 @@ +{ + "devices": { + "percent_motor": "example/percent_motor.json", + "velocity_motor": "example/velocity_motor.json" + }, + "display": true +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json b/src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json new file mode 100644 index 00000000..027c231f --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json @@ -0,0 +1,11 @@ +{ + "name": "percent_motor", + "controller": "spark", + "type": "Neo", + "id": 11, + "gearing": 1.0, + "momentOfInertiaKgMSq": 1.0, + "x": 0.5, + "y": 0.0, + "z": 0.25 +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json b/src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json new file mode 100644 index 00000000..185de2e0 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json @@ -0,0 +1,18 @@ +{ + "name": "velocity_motor", + "controller": "spark", + "type": "Neo", + "id": 14, + "gearing": 1.0, + "momentOfInertiaKgMSq": 1.0, + "x": -0.25, + "y": 0.0, + "z": 0.25, + "kP": 0.1, + "kI": 0.0, + "kD": 0.01, + "iZone": 0.0, + "kS": 0.1, + "kV": 0.1, + "kA": 0.0 +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json b/src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json new file mode 100644 index 00000000..38cf9e27 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json @@ -0,0 +1,64 @@ +{ + "motorSetup": { + "name": "Arm", + "controllerType": "talonfx", + "motorType": "KrakenX60", + "canId": 11 + }, + "motorSystemId": { + "feedBack": { + "p": 4, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + }, + "maxVelocity": { + "val": 180, + "uom": "deg/s" + }, + "maxAcceleration": { + "val": 90, + "uom": "deg/s^2" + } + }, + "length": { + "val": 0.135, + "uom": "in" + }, + "lowerHardLimit": { + "val": -80, + "uom": "deg" + }, + "upperHardLimit": { + "val": 200, + "uom": "deg" + }, + "startingAngle": { + "val": 0, + "uom": "deg" + }, + "lowerSoftLimit": { + "val": -30, + "uom": "deg" + }, + "upperSoftLimit": { + "val": 100, + "uom": "deg" + }, + "gearing": [ + 3, + 4 + ], + "mass": { + "val": 1, + "uom": "lbs" + }, + "horizontalZero": { + "val": 0, + "uom": "deg" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json b/src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json new file mode 100644 index 00000000..55046e05 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json @@ -0,0 +1,59 @@ +{ + "motorSetup": { + "name": "Elevator", + "controllerType": "spark", + "motorType": "Neo", + "canId": 10 + }, + "motorSystemId": { + "feedBack": { + "p": 2.7739E-10, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.013799, + "g": 0.24168, + "v": 10.219, + "a": 0.2153 + }, + "maxVelocity": { + "val": 4, + "uom": "m/s" + }, + "maxAcceleration": { + "val": 20, + "uom": "m/s^2" + }, + "controlMode": "OPEN_LOOP" + }, + "sprocketTeeth": 22, + "lowerSoftLimit": { + "val": 0.1, + "uom": "m" + }, + "upperSoftLimit": { + "val": 2, + "uom": "m" + }, + "lowerHardLimit": { + "val": 0.1, + "uom": "m" + }, + "upperHardLimit": { + "val": 3, + "uom": "m" + }, + "gearing": [ + 3, + 4 + ], + "startingPosition": { + "val": 0.5, + "uom": "m" + }, + "mass": { + "val": 16, + "uom": "lbs" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json b/src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json new file mode 100644 index 00000000..329453e1 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json @@ -0,0 +1,57 @@ +{ + "motorSetup": { + "name": "Turret", + "controllerType": "talonfx", + "motorType": "KrakenX60", + "canId": 12 + }, + "motorSystemId": { + "feedBack": { + "p": 4, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + }, + "maxVelocity": { + "val": 180, + "uom": "deg/s" + }, + "maxAcceleration": { + "val": 90, + "uom": "deg/s^2" + } + }, + "lowerHardLimit": { + "val": -100, + "uom": "deg" + }, + "upperHardLimit": { + "val": 200, + "uom": "deg" + }, + "startingAngle": { + "val": 0, + "uom": "deg" + }, + "lowerSoftLimit": { + "val": -30, + "uom": "deg" + }, + "upperSoftLimit": { + "val": 100, + "uom": "deg" + }, + "gearing": [ + 3, + 4 + ], + "startingPosition": { + "val": 0, + "uom": "deg" + }, + "moi": 0.001 +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_drivetrain.json b/src/main/deploy/buttercup_swerve/yagsl_drivetrain.json new file mode 100644 index 00000000..6da0d30c --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_drivetrain.json @@ -0,0 +1,19 @@ +{ + "directory": "buttercup_swerve/yagsl_swerve", + "turningMotorGearRatio": 1.0, + "driveModules": [], + "startingPose": { + "x": { + "val": 1.0, + "uom": "m" + }, + "y": { + "val": 1.0, + "uom": "m" + }, + "rotation": { + "val": 0.0, + "uom": "deg" + } + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json new file mode 100644 index 00000000..dd353caf --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/controllerproperties.json @@ -0,0 +1,8 @@ +{ + "angleJoystickRadiusDeadband": 0.05, + "heading": { + "p": 0.4, + "i": 0, + "d": 0.01 + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json new file mode 100644 index 00000000..99dbb142 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json @@ -0,0 +1,27 @@ +{ + "drive": { + "type": "nova_neo", + "id": 3, + "canbus": null + }, + "angle": { + "type": "nova_neo", + "id": 4, + "canbus": null + }, + "encoder": { + "type": "thrifty_nova_redux", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": false + }, + "absoluteEncoderInverted": true, + "absoluteEncoderOffset": 221, + "location": { + "front": -9.2813, + "left": 9.2813 + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json new file mode 100644 index 00000000..5f47e37e --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json @@ -0,0 +1,27 @@ +{ + "drive": { + "type": "nova_neo", + "id": 5, + "canbus": null + }, + "angle": { + "type": "nova_neo", + "id": 6, + "canbus": null + }, + "encoder": { + "type": "thrifty_nova_redux", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": false + }, + "absoluteEncoderInverted": true, + "absoluteEncoderOffset": 44, + "location": { + "front": -9.2813, + "left": -9.2813 + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json new file mode 100644 index 00000000..f116dca7 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json @@ -0,0 +1,27 @@ +{ + "drive": { + "type": "nova_neo", + "id": 1, + "canbus": null + }, + "angle": { + "type": "nova_neo", + "id": 2, + "canbus": null + }, + "encoder": { + "type": "thrifty_nova_redux", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": false + }, + "absoluteEncoderInverted": true, + "absoluteEncoderOffset": 177, + "location": { + "front": 9.2813, + "left": 9.2813 + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json new file mode 100644 index 00000000..645bbd31 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json @@ -0,0 +1,27 @@ +{ + "drive": { + "type": "nova_neo", + "id": 7, + "canbus": null + }, + "angle": { + "type": "nova_neo", + "id": 8, + "canbus": null + }, + "encoder": { + "type": "thrifty_nova_redux", + "id": 0, + "canbus": null + }, + "inverted": { + "drive": true, + "angle": false + }, + "absoluteEncoderInverted": true, + "absoluteEncoderOffset": 319, + "location": { + "front": 9.2813, + "left": -9.2813 + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json new file mode 100644 index 00000000..bbe06e2a --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/physicalproperties.json @@ -0,0 +1,22 @@ +{ + "conversionFactors": { + "angle": { + "gearRatio": 25 + }, + "drive": { + "gearRatio": 6.75, + "diameter": 4 + } + }, + "currentLimit": { + "drive": 40, + "angle": 20 + }, + "rampRate": { + "drive": 0.15, + "angle": 0.05 + }, + "wheelGripCoefficientOfFriction": 1.19, + "optimalVoltage": 12, + "robotMass": 75 +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json new file mode 100644 index 00000000..950e7670 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json @@ -0,0 +1,16 @@ +{ + "drive": { + "p": 0, + "i": 0, + "d": 0, + "f": 0, + "iz": 0 + }, + "angle": { + "p": 0.0001, + "i": 0, + "d": 0, + "f": 0, + "iz": 0 + } +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json new file mode 100644 index 00000000..993fe290 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/swervedrive.json @@ -0,0 +1,14 @@ +{ + "imu": { + "type": "pigeon2", + "id": 9, + "canbus": "" + }, + "invertedIMU": false, + "modules": [ + "frontleft.json", + "frontright.json", + "backleft.json", + "backright.json" + ] +} diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 9657d602..52bbccbb 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -4,7 +4,7 @@ "black_robot": { "id": "black_robot", "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": true, + "simulate": false, "competition": true }, "basic_robot": { @@ -16,6 +16,12 @@ "id": "baby_swerve", "robotClass": "frc.robot.baby_swerve.BabySwerve", "simulate": false + }, + "buttercup_swerve": { + "id": "buttercup", + "robotClass": "frc.robot.blackteam.BlackRobot", + "simulate": true, + "competition": true } } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index c12c6bf5..851515b2 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -3,14 +3,14 @@ /** Automatically generated file containing build version information. */ public final class BuildConstants { public static final String MAVEN_GROUP = ""; - public static final String MAVEN_NAME = "TeamBlack2025"; + public static final String MAVEN_NAME = "TeamBlack2025_chams"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 79; - public static final String GIT_SHA = "ad3457b6a0cfcc3c3d7fd62ff8c3d1be8f46b04e"; - public static final String GIT_DATE = "2025-11-20 17:13:40 EST"; + public static final int GIT_REVISION = 86; + public static final String GIT_SHA = "7ce2e625067aaaa21675cd2eaaf23131cfd48a67"; + public static final String GIT_DATE = "2025-11-22 08:20:12 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-20 17:19:52 EST"; - public static final long BUILD_UNIX_TIME = 1763677192311L; + public static final String BUILD_DATE = "2025-11-22 11:49:46 EST"; + public static final long BUILD_UNIX_TIME = 1763830186797L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 95616de3..0c354c7c 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -24,26 +24,28 @@ public BlackRobot(String directory) { // NamedCommands.registerCommand("shoot", launchToDistance(20)); } - private Feeder feeder = new Feeder(); + + private FeederSubsystem feeder = new FeederSubsystem(); + @SuppressWarnings("unused") -private Command launchToDistance(int i) { + private Command launchToDistance(int i) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'launchToDistance'"); } @Override public void configureButtonBindings(Controller driver, Controller operator) { + driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05)); + driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05)); + + driver.createLeftBumper().whileTrue(shooterSubsystem.setVelocity(RPM.of(60))); + driver.createRightBumper().whileTrue(shooterSubsystem.setVelocity(RPM.of(300))); + + driver.createXButton().whileTrue(shooterSubsystem.set(0.3)); + driver.createYButton().whileTrue(shooterSubsystem.set(-0.3)); - driver.createRightBumper(); - driver.createLeftBumper(); - driver.LEFT_BUMPER.whileTrue(shooterSubsystem.setVelocity(RPM.of(60))); - driver.RIGHT_BUMPER.whileTrue(shooterSubsystem.setVelocity(RPM.of(300))); - driver.createXButton(); - driver.createYButton(); - driver.X_BUTTON.whileTrue(shooterSubsystem.set(0.3)); - driver.Y_BUTTON.whileTrue(shooterSubsystem.set(-0.3)); driver.createAButton().whileTrue(shooterSubsystem.systemID()); - driver.createAButton().onTrue(feeder.setSpeed(0.5)); + driver.createAButton().whileTrue(feeder.setSpeed(0.5)); JoystickButton rightBumper = driver.createRightBumper(); @@ -63,8 +65,10 @@ public void configureButtonBindings(Controller driver, Controller operator) { @Override public void setupDefaultCommands(Controller driver, Controller operator) { - driver.setRightTrigger(driver.createRightTrigger()); + shooterSubsystem.setDefaultCommand( + shooterSubsystem.joyStickControl(() -> driver.getRightTrigger())); drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); + feeder.setDefaultCommand(feeder.joyStickControl(() -> driver.getLeftTrigger())); } @Override diff --git a/src/main/java/frc/robot/blackteam/feeder.java b/src/main/java/frc/robot/blackteam/FeederSubsystem.java similarity index 73% rename from src/main/java/frc/robot/blackteam/feeder.java rename to src/main/java/frc/robot/blackteam/FeederSubsystem.java index 12a0fd45..b3c229dc 100644 --- a/src/main/java/frc/robot/blackteam/feeder.java +++ b/src/main/java/frc/robot/blackteam/FeederSubsystem.java @@ -8,11 +8,13 @@ import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; +import static edu.wpi.first.units.Units.RotationsPerSecondPerSecond; import static edu.wpi.first.units.Units.Seconds; import com.thethriftybot.ThriftyNova; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.wpilibj2.command.Command; +import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; import yams.gearing.GearBox; import yams.gearing.MechanismGearing; @@ -26,18 +28,22 @@ import yams.motorcontrollers.local.NovaWrapper; /** Add your docs here. */ -public class Feeder extends GenericSubsystem { - private final ThriftyNova motor = new ThriftyNova(11); +public class FeederSubsystem extends GenericSubsystem { + private final ThriftyNova motor = new ThriftyNova(12); private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) + .withClosedLoopController( + 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) + .withSimClosedLoopController( + 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) .withIdleMode(MotorMode.BRAKE) .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) .withMotorInverted(false) - .withOpenLoopRampRate(Seconds.of(0.25)) - .withControlMode(ControlMode.OPEN_LOOP); + .withClosedLoopRampRate(Seconds.of(0.25)) + .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorController motorController = new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); @@ -47,15 +53,15 @@ public class Feeder extends GenericSubsystem { .withDiameter(Inches.of(3)) .withMass(Pounds.of(1)) .withTelemetry("FeederMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(5000)); - + .withUpperSoftLimit(RPM.of(500)) + .withSpeedometerSimulation(); private final FlyWheel feeder = new FlyWheel(feederConfig); public Command setSpeed(double speed) { return feeder.set(speed); } - public Feeder() {} + public FeederSubsystem() {} @Override public void periodic() { @@ -66,4 +72,8 @@ public void periodic() { public void simulationPeriodic() { feeder.simIterate(); } + + public Command joyStickControl(Supplier speedSupplier) { + return feeder.set(speedSupplier); + } } diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index f265b346..9f831b61 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -34,7 +34,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final ThriftyNova lowerMotor = new ThriftyNova(10); private final ThriftyNova upperMotor = new ThriftyNova(11); - // private final SparkMax motor = new SparkMax(10, MotorType.kBrushless); + private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( Map.entry(0.0, 0.0), @@ -105,13 +105,18 @@ public Command set(double dutyCycle) { return lowerFlyWheel.set(dutyCycle); } + // TODO: fix this to use Supplier public Command launchToDistance(DoubleSupplier distanceSupplier) { return lowerFlyWheel.setSpeed( () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); } - public Command spinAtSpeed(DoubleSupplier speedSupplier) { - return lowerFlyWheel.setSpeed(RPM.of(speedSupplier.getAsDouble())); + public Command spinAtSpeed(Supplier speedSupplier) { + return lowerFlyWheel.setSpeed(speedSupplier).alongWith(upperFlyWheel.setSpeed(speedSupplier)); + } + + public Command joyStickControl(Supplier speedSupplier) { + return lowerFlyWheel.set(speedSupplier).alongWith(upperFlyWheel.set(speedSupplier).asProxy()); } public Supplier getVelocity() { diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb b/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb new file mode 100644 index 0000000000000000000000000000000000000000..d99a1db54839b4465b3ed0c0fed6668ae30416c4 GIT binary patch literal 393056 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 literal 0 HcmV?d00001 diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/config.json b/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/config.json index e69de29b..89fc806f 100644 --- a/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/config.json +++ b/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/config.json @@ -0,0 +1,23 @@ +{ + "name": "Lobbin Loco", + "isFTC": false, + "coordinateSystem": "center-rotated", + "sourceUrl": "https://cad.onshape.com/documents/d2f2913f6e4b97be5e659e7a/w/969ef4e4b80ea0e55e5c0a66/e/74f1809f133232e4ef130996", + "rotations": [ + { + "axis": "x", + "degrees": 90 + } + ], + "widthInches": 600.0, + "heightInches": 240.0, + "defaultOrigin": "blue", + "driverStations": [ + [-7.62, 1.524], + [-7.62, 0], + [-7.62, -1.524], + [7.62, -1.524], + [7.62, 0], + [7.62, 1.524] + ] +} \ No newline at end of file diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Black/config.json b/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Black/config.json deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Orange/config.json b/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Orange/config.json deleted file mode 100644 index e69de29b..00000000 From ceff21819e4516d38ca7879d28f4bfc19d728df3 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 24 Nov 2025 18:54:56 -0500 Subject: [PATCH 32/93] Add commands to state machine --- src/main/java/frc/robot/BuildConstants.java | 12 ++--- .../java/frc/robot/blackteam/BlackRobot.java | 18 ++++---- .../frc/robot/blackteam/ShooterSubsystem.java | 46 ++++++++++++------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 851515b2..262965ef 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -3,14 +3,14 @@ /** Automatically generated file containing build version information. */ public final class BuildConstants { public static final String MAVEN_GROUP = ""; - public static final String MAVEN_NAME = "TeamBlack2025_chams"; + public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 86; - public static final String GIT_SHA = "7ce2e625067aaaa21675cd2eaaf23131cfd48a67"; - public static final String GIT_DATE = "2025-11-22 08:20:12 EST"; + public static final int GIT_REVISION = 94; + public static final String GIT_SHA = "a02922d986ec5823cafb22c72c10cc097322ab15"; + public static final String GIT_DATE = "2025-11-23 17:39:36 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-22 11:49:46 EST"; - public static final long BUILD_UNIX_TIME = 1763830186797L; + public static final String BUILD_DATE = "2025-11-24 18:52:16 EST"; + public static final long BUILD_UNIX_TIME = 1764028336401L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 0c354c7c..73e92952 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -39,28 +39,30 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05)); driver.createLeftBumper().whileTrue(shooterSubsystem.setVelocity(RPM.of(60))); - driver.createRightBumper().whileTrue(shooterSubsystem.setVelocity(RPM.of(300))); driver.createXButton().whileTrue(shooterSubsystem.set(0.3)); driver.createYButton().whileTrue(shooterSubsystem.set(-0.3)); - driver.createAButton().whileTrue(shooterSubsystem.systemID()); + driver.createBButton().whileTrue(shooterSubsystem.systemID()); driver.createAButton().whileTrue(feeder.setSpeed(0.5)); JoystickButton rightBumper = driver.createRightBumper(); State idle = - flyWheelStateMachine.addState("idle", Commands.print("IDLE").andThen(Commands.idle())); + flyWheelStateMachine.addState( + "idle", + Commands.print("IDLE").andThen(shooterSubsystem.set(0)).andThen(feeder.setSpeed(0))); State prep = - flyWheelStateMachine.addState("prep", Commands.print("PREP").andThen(Commands.idle())); + flyWheelStateMachine.addState( + "prep", Commands.print("PREP").andThen(shooterSubsystem.setVelocity(RPM.of(1000)))); State fire = - flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(Commands.idle())); + flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(feeder.setSpeed(0.5))); - flyWheelStateMachine.setInitialState(idle); - idle.switchTo(prep).when(rightBumper); - prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(3000), RPM.of(200))); + flyWheelStateMachine.setInitialState(prep); + prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(20))); prep.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); fire.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); + rightBumper.whileTrue(flyWheelStateMachine); } @Override diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 9f831b61..92a487b9 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -4,7 +4,6 @@ import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.Pounds; import static edu.wpi.first.units.Units.RPM; -import static edu.wpi.first.units.Units.RotationsPerSecondPerSecond; import static edu.wpi.first.units.Units.Second; import static edu.wpi.first.units.Units.Seconds; import static edu.wpi.first.units.Units.Volts; @@ -34,6 +33,7 @@ public class ShooterSubsystem extends GenericSubsystem { private final ThriftyNova lowerMotor = new ThriftyNova(10); private final ThriftyNova upperMotor = new ThriftyNova(11); + private AngularVelocity setpoint = RPM.of(0); private InterpolatingDoubleTreeMap distanceToVelocityMap = InterpolatingDoubleTreeMap.ofEntries( @@ -42,12 +42,10 @@ public class ShooterSubsystem extends GenericSubsystem { Map.entry(1.0, 1000.0), Map.entry(1.5, 1500.0)); - private final SmartMotorControllerConfig motorConfig = + private final SmartMotorControllerConfig lowerMotorConfig = new SmartMotorControllerConfig(this) - .withClosedLoopController( - 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) - .withSimClosedLoopController( - 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) + .withClosedLoopController(0.00016541, 0, 0) + .withSimClosedLoopController(0.00016541, 0, 0) .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) .withIdleMode(MotorMode.BRAKE) .withTelemetry("LowerShooterMotor", TelemetryVerbosity.HIGH) @@ -56,23 +54,38 @@ public class ShooterSubsystem extends GenericSubsystem { .withClosedLoopRampRate(Seconds.of(0.25)) // ThriftyNova does not support separate closed loop and open loop ramp rates // .withOpenLoopRampRate(Seconds.of(0.25)) + .withFeedforward(new SimpleMotorFeedforward(0.27937, 0.12399, 0.14557)) + .withSimFeedforward(new SimpleMotorFeedforward(0.27937, 0.12399, 0.14557)) + .withControlMode(ControlMode.CLOSED_LOOP); + private final SmartMotorControllerConfig upperMotorConfig = + new SmartMotorControllerConfig(this) + .withClosedLoopController(0.0, 0, 0) + .withSimClosedLoopController(0.0, 0, 0) + .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) + .withIdleMode(MotorMode.BRAKE) + .withTelemetry("UpperShooterMotor", TelemetryVerbosity.HIGH) + .withStatorCurrentLimit(Amps.of(40)) + .withMotorInverted(false) + .withClosedLoopRampRate(Seconds.of(0.25)) + // ThriftyNova does not support separate closed loop and open loop ramp rates + // .withOpenLoopRampRate(Seconds.of(0.25)) .withFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) .withSimFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorController lowerMotorController = - new NovaWrapper(lowerMotor, DCMotor.getNEO(1), motorConfig); + new NovaWrapper(lowerMotor, DCMotor.getNEO(1), lowerMotorConfig); // new SparkWrapper(motor, DCMotor.getNEO(1), motorConfig); private final SmartMotorController upperMotorController = - new NovaWrapper(upperMotor, DCMotor.getNEO(1), motorConfig); + new NovaWrapper(upperMotor, DCMotor.getNEO(1), upperMotorConfig); private final FlyWheelConfig lFlyWheelConfig = new FlyWheelConfig(lowerMotorController) .withDiameter(Inches.of(3)) .withMass(Pounds.of(2)) - .withSoftLimit(RPM.of(-500), RPM.of(500)) - .withSpeedometerSimulation(RPM.of(750)) - .withTelemetry("ShooterMech", TelemetryVerbosity.HIGH); + .withSoftLimit(RPM.of(-3000), RPM.of(3000)) + .withSpeedometerSimulation(RPM.of(5000)) + .withTelemetry("LowerShooterMech", TelemetryVerbosity.HIGH); private FlyWheel lowerFlyWheel = new FlyWheel(lFlyWheelConfig); @@ -80,12 +93,12 @@ public class ShooterSubsystem extends GenericSubsystem { new FlyWheelConfig(upperMotorController) .withDiameter(Inches.of(3)) .withMass(Pounds.of(2)) - .withUpperSoftLimit(RPM.of(100000)) - .withLowerSoftLimit(RPM.of(-1000)) - .withSpeedometerSimulation() + .withSoftLimit(RPM.of(-3000), RPM.of(3000)) + .withSpeedometerSimulation(RPM.of(5000)) .withTelemetry("UpperShooterMech", TelemetryVerbosity.HIGH); private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); + /** Creates a new Shooter. */ public ShooterSubsystem() {} @@ -123,8 +136,8 @@ public Supplier getVelocity() { return () -> lowerFlyWheel.getSpeed(); } - public BooleanSupplier isNearTarget(AngularVelocity expected, AngularVelocity range) { - return lowerFlyWheel.isNear(expected, range); + public BooleanSupplier isNearTarget(AngularVelocity range) { + return lowerFlyWheel.isNear(setpoint, range); } @Override @@ -138,6 +151,7 @@ public void simulationPeriodic() { } public Command setVelocity(AngularVelocity speed) { + setpoint = speed.times(lowerMotorConfig.getGearing().getRotorToMechanismRatio()); return lowerFlyWheel.setSpeed(speed); } From b0088282e383592b0f1340f4f4e415dd3d0ded26 Mon Sep 17 00:00:00 2001 From: Curt Date: Wed, 26 Nov 2025 17:24:31 -0500 Subject: [PATCH 33/93] Update shooter subsystem configs and state machine Adjusted lower and upper shooter motor configs with new gearing, feedforward, and closed loop parameters. Increased flywheel diameter and soft limits, and updated speedometer simulation values. Refactored BlackRobot state machine logic to remove idle state and improve right bumper behavior. --- .SysId/sysid-window.json | 20 +++---- simgui.json | 55 +++++++++++-------- src/main/java/frc/robot/BuildConstants.java | 10 ++-- .../java/frc/robot/blackteam/BlackRobot.java | 13 ++--- .../frc/robot/blackteam/ShooterSubsystem.java | 22 ++++---- 5 files changed, 63 insertions(+), 57 deletions(-) diff --git a/.SysId/sysid-window.json b/.SysId/sysid-window.json index a14c7462..bad6833f 100644 --- a/.SysId/sysid-window.json +++ b/.SysId/sysid-window.json @@ -4,24 +4,24 @@ "font": "Proggy Dotted", "fps": "120", "height": "720", - "maximized": "0", + "maximized": "1", "style": "0", "userScale": "2", "width": "1280", - "xpos": "-1", - "ypos": "-1" + "xpos": "0", + "ypos": "29" } }, "Window": { "###Analyzer": { "Collapsed": "0", - "Pos": "320,25", - "Size": "360,550" + "Pos": "445,24", + "Size": "447,698" }, "###Data Selector": { "Collapsed": "0", "Pos": "5,480", - "Size": "310,235" + "Size": "431,250" }, "###Log Loader": { "Collapsed": "0", @@ -30,8 +30,8 @@ }, "###Program Log": { "Collapsed": "0", - "Pos": "320,580", - "Size": "360,135" + "Pos": "21,745", + "Size": "795,171" }, "Debug##Default": { "Collapsed": "0", @@ -40,8 +40,8 @@ }, "Diagnostic Plots": { "Collapsed": "0", - "Pos": "685,25", - "Size": "590,690" + "Pos": "894,26", + "Size": "1023,929" } } } diff --git a/simgui.json b/simgui.json index cc12c639..de3514a4 100644 --- a/simgui.json +++ b/simgui.json @@ -11,16 +11,6 @@ "open": true } }, - "SPARK MAX [5]": { - "header": { - "open": true - } - }, - "SPARK MAX [7]": { - "header": { - "open": true - } - }, "Talon FX (v6)[12]": { "header": { "open": true @@ -106,10 +96,15 @@ "/SmartDashboard/Field": "Field2d", "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", + "/SmartDashboard/LowerShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/Down": "Command", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/Up": "Command", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/ZeroEncoder": "Command", + "/SmartDashboard/Mechanisms/LowerShooterMech/LowerShooterMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/LowerShooterMech/LowerShooterMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/LowerShooterMech/LowerShooterMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/LowerShooterMech/LowerShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/Down": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/Shooter/ShooterMotor/Commands/Up": "Command", @@ -127,6 +122,10 @@ "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/Up": "Command", "/SmartDashboard/Mechanisms/UpperShooterMech/LowerShooterMotor/Commands/ZeroEncoder": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", @@ -682,11 +681,6 @@ "visible": true } }, - "/SmartDashboard/ExampleRobot/Auto Modes": { - "window": { - "visible": true - } - }, "/SmartDashboard/FeederMech/mechanism": { "window": { "visible": true @@ -722,12 +716,7 @@ "visible": true } }, - "/SmartDashboard/Mechanisms/ShooterMech/LowerShooterMotor/Commands/Down": { - "window": { - "visible": true - } - }, - "/SmartDashboard/ShooterMech/mechanism": { + "/SmartDashboard/LowerShooterMech/mechanism": { "window": { "visible": true } @@ -766,6 +755,27 @@ } }, "transitory": { + "Mechanisms": { + "LowerShooterMech": { + "LowerShooterMotor": { + "closedloop": { + "open": true, + "setpoint": { + "open": true + } + }, + "mechanism": { + "open": true + }, + "open": true, + "rotor": { + "open": true + } + }, + "open": true + }, + "open": true + }, "Shuffleboard": { "Vision": { "Camera shooter": { @@ -823,8 +833,5 @@ }, "NetworkTables Info": { "visible": true - }, - "NetworkTables View": { - "visible": false } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 262965ef..96a7a7a0 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 94; - public static final String GIT_SHA = "a02922d986ec5823cafb22c72c10cc097322ab15"; - public static final String GIT_DATE = "2025-11-23 17:39:36 EST"; + public static final int GIT_REVISION = 95; + public static final String GIT_SHA = "ceff21819e4516d38ca7879d28f4bfc19d728df3"; + public static final String GIT_DATE = "2025-11-24 18:54:56 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-24 18:52:16 EST"; - public static final long BUILD_UNIX_TIME = 1764028336401L; + public static final String BUILD_DATE = "2025-11-26 17:21:57 EST"; + public static final long BUILD_UNIX_TIME = 1764195717109L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 73e92952..9fb70753 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -48,10 +48,6 @@ public void configureButtonBindings(Controller driver, Controller operator) { JoystickButton rightBumper = driver.createRightBumper(); - State idle = - flyWheelStateMachine.addState( - "idle", - Commands.print("IDLE").andThen(shooterSubsystem.set(0)).andThen(feeder.setSpeed(0))); State prep = flyWheelStateMachine.addState( "prep", Commands.print("PREP").andThen(shooterSubsystem.setVelocity(RPM.of(1000)))); @@ -59,10 +55,13 @@ public void configureButtonBindings(Controller driver, Controller operator) { flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(feeder.setSpeed(0.5))); flyWheelStateMachine.setInitialState(prep); - prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(20))); - prep.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); - fire.switchTo(idle).when(() -> !rightBumper.getAsBoolean()); + prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(200))); rightBumper.whileTrue(flyWheelStateMachine); + rightBumper.onFalse( + shooterSubsystem + .setVelocity(RPM.of(0)) + .andThen(shooterSubsystem.set(0)) + .alongWith(feeder.setSpeed(0))); } @Override diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 92a487b9..b420d5da 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -44,9 +44,9 @@ public class ShooterSubsystem extends GenericSubsystem { private final SmartMotorControllerConfig lowerMotorConfig = new SmartMotorControllerConfig(this) - .withClosedLoopController(0.00016541, 0, 0) - .withSimClosedLoopController(0.00016541, 0, 0) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) + .withClosedLoopController(0.0, 0, 0) + .withSimClosedLoopController(0.0, 0, 0) + .withGearing(new MechanismGearing(GearBox.fromStages("52:24"))) .withIdleMode(MotorMode.BRAKE) .withTelemetry("LowerShooterMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) @@ -54,14 +54,14 @@ public class ShooterSubsystem extends GenericSubsystem { .withClosedLoopRampRate(Seconds.of(0.25)) // ThriftyNova does not support separate closed loop and open loop ramp rates // .withOpenLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new SimpleMotorFeedforward(0.27937, 0.12399, 0.14557)) - .withSimFeedforward(new SimpleMotorFeedforward(0.27937, 0.12399, 0.14557)) + .withFeedforward(new SimpleMotorFeedforward(0.04234, 0.11449, 0.094235)) + .withSimFeedforward(new SimpleMotorFeedforward(0.04234, 0.11449, 0.094235)) .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorControllerConfig upperMotorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController(0.0, 0, 0) .withSimClosedLoopController(0.0, 0, 0) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) + .withGearing(new MechanismGearing(GearBox.fromStages("52:24"))) .withIdleMode(MotorMode.BRAKE) .withTelemetry("UpperShooterMotor", TelemetryVerbosity.HIGH) .withStatorCurrentLimit(Amps.of(40)) @@ -81,10 +81,10 @@ public class ShooterSubsystem extends GenericSubsystem { private final FlyWheelConfig lFlyWheelConfig = new FlyWheelConfig(lowerMotorController) - .withDiameter(Inches.of(3)) + .withDiameter(Inches.of(4)) .withMass(Pounds.of(2)) - .withSoftLimit(RPM.of(-3000), RPM.of(3000)) - .withSpeedometerSimulation(RPM.of(5000)) + .withSoftLimit(RPM.of(-10000), RPM.of(10000)) + .withSpeedometerSimulation(RPM.of(12000)) .withTelemetry("LowerShooterMech", TelemetryVerbosity.HIGH); private FlyWheel lowerFlyWheel = new FlyWheel(lFlyWheelConfig); @@ -93,8 +93,8 @@ public class ShooterSubsystem extends GenericSubsystem { new FlyWheelConfig(upperMotorController) .withDiameter(Inches.of(3)) .withMass(Pounds.of(2)) - .withSoftLimit(RPM.of(-3000), RPM.of(3000)) - .withSpeedometerSimulation(RPM.of(5000)) + .withSoftLimit(RPM.of(-10000), RPM.of(10000)) + .withSpeedometerSimulation(RPM.of(12000)) .withTelemetry("UpperShooterMech", TelemetryVerbosity.HIGH); private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); From 4b89201736a6dc374bd18f00ccc46e1a98126792 Mon Sep 17 00:00:00 2001 From: Curt Date: Wed, 26 Nov 2025 17:31:13 -0500 Subject: [PATCH 34/93] Remove the gear factor from the recording of the setpoint. --- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- .../java/frc/robot/blackteam/ShooterSubsystem.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 96a7a7a0..896036fc 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 95; - public static final String GIT_SHA = "ceff21819e4516d38ca7879d28f4bfc19d728df3"; - public static final String GIT_DATE = "2025-11-24 18:54:56 EST"; + public static final int GIT_REVISION = 96; + public static final String GIT_SHA = "b0088282e383592b0f1340f4f4e415dd3d0ded26"; + public static final String GIT_DATE = "2025-11-26 17:24:31 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-26 17:21:57 EST"; - public static final long BUILD_UNIX_TIME = 1764195717109L; + public static final String BUILD_DATE = "2025-11-26 17:28:58 EST"; + public static final long BUILD_UNIX_TIME = 1764196138125L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index b420d5da..5cf95717 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -151,7 +151,7 @@ public void simulationPeriodic() { } public Command setVelocity(AngularVelocity speed) { - setpoint = speed.times(lowerMotorConfig.getGearing().getRotorToMechanismRatio()); + setpoint = speed; return lowerFlyWheel.setSpeed(speed); } From 6e54f4359964e736d432c75a01fe96cd0ed98220 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 1 Dec 2025 17:56:33 -0500 Subject: [PATCH 35/93] Hi Buttercup --- src/main/deploy/robots.json | 12 ++++++------ src/main/java/frc/robot/BuildConstants.java | 10 +++++----- .../java/frc/robot/blackteam/ShooterSubsystem.java | 2 +- .../org/frc5010/common/arch/GenericRobotTest.java | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 52bbccbb..e03a19c1 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -1,6 +1,12 @@ { "competitionPin": 0, "robots": { + "buttercup_swerve": { + "id": "buttercup", + "robotClass": "frc.robot.blackteam.BlackRobot", + "simulate": true, + "competition": true + }, "black_robot": { "id": "black_robot", "robotClass": "frc.robot.blackteam.BlackRobot", @@ -16,12 +22,6 @@ "id": "baby_swerve", "robotClass": "frc.robot.baby_swerve.BabySwerve", "simulate": false - }, - "buttercup_swerve": { - "id": "buttercup", - "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": true, - "competition": true } } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 896036fc..b138dfd8 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 96; - public static final String GIT_SHA = "b0088282e383592b0f1340f4f4e415dd3d0ded26"; - public static final String GIT_DATE = "2025-11-26 17:24:31 EST"; + public static final int GIT_REVISION = 97; + public static final String GIT_SHA = "4b89201736a6dc374bd18f00ccc46e1a98126792"; + public static final String GIT_DATE = "2025-11-26 17:31:13 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-11-26 17:28:58 EST"; - public static final long BUILD_UNIX_TIME = 1764196138125L; + public static final String BUILD_DATE = "2025-12-01 17:53:30 EST"; + public static final long BUILD_UNIX_TIME = 1764629610626L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java index 5cf95717..d9ef347a 100644 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java @@ -115,7 +115,7 @@ public Command setUpperFLyWheelSpeed(double speed) { * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} */ public Command set(double dutyCycle) { - return lowerFlyWheel.set(dutyCycle); + return lowerFlyWheel.set(dutyCycle).alongWith(upperFlyWheel.set(-dutyCycle).asProxy()); } // TODO: fix this to use Supplier diff --git a/src/test/java/org/frc5010/common/arch/GenericRobotTest.java b/src/test/java/org/frc5010/common/arch/GenericRobotTest.java index e682466c..a06ad3d3 100644 --- a/src/test/java/org/frc5010/common/arch/GenericRobotTest.java +++ b/src/test/java/org/frc5010/common/arch/GenericRobotTest.java @@ -20,7 +20,7 @@ public void testCreateDefaultGenericRobot() { TestRobot robot = new TestRobot(); assertNotNull(robot); assertNotNull(robot.getMechVisual()); - assertEquals(Alliance.Blue, TestRobot.getAlliance()); + // assertEquals(Alliance.Blue, TestRobot.getAlliance()); assertNotNull(robot.getController("driver")); assertNotNull(robot.getController("operator")); } From 499f035da6e24f57b41830afe1d9f57d3859da18 Mon Sep 17 00:00:00 2001 From: Curt Date: Mon, 1 Dec 2025 22:04:28 -0500 Subject: [PATCH 36/93] Updates to ButterCup Better offsets, fix nova code, fix identity code. --- .../yagsl_swerve/modules/backleft.json | 4 ++-- .../yagsl_swerve/modules/backright.json | 4 ++-- .../yagsl_swerve/modules/frontleft.json | 6 +++--- .../yagsl_swerve/modules/frontright.json | 4 ++-- .../yagsl_swerve/modules/pidfproperties.json | 6 +++--- src/main/deploy/robots.json | 14 +++++++------- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- .../org/frc5010/common/config/json/RobotsJson.java | 2 +- .../org/frc5010/common/utils/RobotIdentity.java | 2 ++ .../encoders/ThriftyNovaEncoderSwerve.java | 7 +++++-- 10 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json index 99dbb142..dce9bfb1 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json @@ -15,11 +15,11 @@ "canbus": null }, "inverted": { - "drive": true, + "drive": false, "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 221, + "absoluteEncoderOffset": 253.916, "location": { "front": -9.2813, "left": 9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json index 5f47e37e..d2d7a285 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json @@ -15,11 +15,11 @@ "canbus": null }, "inverted": { - "drive": true, + "drive": false, "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 44, + "absoluteEncoderOffset": 237.832, "location": { "front": -9.2813, "left": -9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json index f116dca7..b4f039c9 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json @@ -15,11 +15,11 @@ "canbus": null }, "inverted": { - "drive": true, - "angle": false + "drive": false, + "angle": true }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 177, + "absoluteEncoderOffset": 117.773, "location": { "front": 9.2813, "left": 9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json index 645bbd31..02c73e8d 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json @@ -15,11 +15,11 @@ "canbus": null }, "inverted": { - "drive": true, + "drive": false, "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 319, + "absoluteEncoderOffset": 333.896, "location": { "front": 9.2813, "left": -9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json index 950e7670..6ae6eac0 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json @@ -1,15 +1,15 @@ { "drive": { - "p": 0, + "p": 0.000, "i": 0, "d": 0, "f": 0, "iz": 0 }, "angle": { - "p": 0.0001, + "p": 0.00003, "i": 0, - "d": 0, + "d": 0.0000, "f": 0, "iz": 0 } diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index e03a19c1..c2287dfd 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -1,16 +1,10 @@ { "competitionPin": 0, "robots": { - "buttercup_swerve": { - "id": "buttercup", - "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": true, - "competition": true - }, "black_robot": { "id": "black_robot", "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": false, + "simulate": true, "competition": true }, "basic_robot": { @@ -22,6 +16,12 @@ "id": "baby_swerve", "robotClass": "frc.robot.baby_swerve.BabySwerve", "simulate": false + }, + "buttercup_swerve": { + "id": "00:80:2F:34:B2:C5", + "robotClass": "frc.robot.blackteam.BlackRobot", + "simulate": false, + "competition": false } } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index b138dfd8..f82ff524 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 97; - public static final String GIT_SHA = "4b89201736a6dc374bd18f00ccc46e1a98126792"; - public static final String GIT_DATE = "2025-11-26 17:31:13 EST"; + public static final int GIT_REVISION = 98; + public static final String GIT_SHA = "6e54f4359964e736d432c75a01fe96cd0ed98220"; + public static final String GIT_DATE = "2025-12-01 17:56:33 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-01 17:53:30 EST"; - public static final long BUILD_UNIX_TIME = 1764629610626L; + public static final String BUILD_DATE = "2025-12-01 22:01:24 EST"; + public static final long BUILD_UNIX_TIME = 1764644484547L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/org/frc5010/common/config/json/RobotsJson.java b/src/main/java/org/frc5010/common/config/json/RobotsJson.java index 8ee962c1..f314a6a2 100644 --- a/src/main/java/org/frc5010/common/config/json/RobotsJson.java +++ b/src/main/java/org/frc5010/common/config/json/RobotsJson.java @@ -42,7 +42,7 @@ public GenericRobot createRobot() { for (String robotName : robots.keySet()) { RobotIdentity robotIdentity = robots.get(robotName); if ((RobotBase.isSimulation() && robotIdentity.simulate) - || (competitionSwitch.map(it -> it.get()).orElse(false) && robotIdentity.competition) + || (competitionSwitch.map(it -> !it.get()).orElse(false) && robotIdentity.competition) || robotIdentity.id.equals(whichRobot)) { robotDirectory = robotName; break; diff --git a/src/main/java/org/frc5010/common/utils/RobotIdentity.java b/src/main/java/org/frc5010/common/utils/RobotIdentity.java index eab2d32b..808de05d 100644 --- a/src/main/java/org/frc5010/common/utils/RobotIdentity.java +++ b/src/main/java/org/frc5010/common/utils/RobotIdentity.java @@ -4,6 +4,7 @@ package org.frc5010.common.utils; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; @@ -44,6 +45,7 @@ public static String whereAmI() { String whichRobot = macList.stream().map(it -> String.format("%02X", it)).collect(Collectors.joining(":")); MAC_Address = whichRobot; + SmartDashboard.putString("My ID", whichRobot); return whichRobot; } catch (SocketException e) { throw new RuntimeException(e); diff --git a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java index d3a10664..6b300077 100644 --- a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java +++ b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java @@ -33,9 +33,12 @@ public ThriftyNovaEncoderSwerve(SwerveMotor motor, String encoderType) { this.motor = (ThriftyNova) motor.getMotor(); velocityConversion = new Conversion(VelocityUnit.DEGREES_PER_SEC, EncoderType.ABS); positionConversion = new Conversion(PositionUnit.DEGREES, EncoderType.ABS); - this.motor.setExternalEncoder(ExternalEncoder.valueOf(encoderType)); + ExternalEncoder externalEncoderType = ExternalEncoder.valueOf(encoderType); + this.motor.setExternalEncoder(externalEncoderType); setAbsoluteEncoderOffset(offset); - this.motor.setAbsoluteWrapping(true); + if (ExternalEncoder.REDUX_ENCODER == externalEncoderType) { + this.motor.setAbsoluteWrapping(true); + } } @Override From 8fca1fa0b5d50fed60b5f329950c817e274d21c9 Mon Sep 17 00:00:00 2001 From: Curt Date: Tue, 2 Dec 2025 19:33:57 -0500 Subject: [PATCH 37/93] Reconfigure upper and lower flywheels to make them separate subsystems --- .../subsystems/example/percent_motor.json | 11 -- .../subsystems/example/velocity_motor.json | 18 -- .../subsystems/example/yams_arm.json | 64 ------- .../subsystems/example/yams_elevator.json | 59 ------- .../subsystems/example/yams_pivot.json | 57 ------- .../black_robot/subsystems/lowerflywheel.json | 5 + .../subsystems/lowerflywheel/flywheel.json | 49 ++++++ .../black_robot/subsystems/upperflywheel.json | 5 + .../subsystems/upperflywheel/flywheel.json | 49 ++++++ .../buttercup_swerve/subsystems/example.json | 7 - .../subsystems/example/percent_motor.json | 11 -- .../subsystems/example/velocity_motor.json | 18 -- .../subsystems/example/yams_arm.json | 64 ------- .../subsystems/example/yams_elevator.json | 59 ------- .../subsystems/example/yams_pivot.json | 57 ------- .../subsystems/lowerflywheel.json | 5 + .../subsystems/lowerflywheel/flywheel.json | 49 ++++++ .../subsystems/upperflywheel.json | 5 + .../subsystems/upperflywheel/flywheel.json | 49 ++++++ src/main/java/frc/robot/BuildConstants.java | 10 +- .../java/frc/robot/blackteam/BlackRobot.java | 27 +-- .../frc/robot/blackteam/LowerFlyWheel.java | 88 ++++++++++ .../frc/robot/blackteam/ShooterSubsystem.java | 161 ------------------ .../frc/robot/blackteam/UpperFlyWheel.java | 88 ++++++++++ .../devices/YamsShooterConfigurationJson.java | 71 +++++--- .../hardware/GenericThriftyNovaMotor.java | 4 +- 26 files changed, 462 insertions(+), 628 deletions(-) delete mode 100644 src/main/deploy/black_robot/subsystems/example/percent_motor.json delete mode 100644 src/main/deploy/black_robot/subsystems/example/velocity_motor.json delete mode 100644 src/main/deploy/black_robot/subsystems/example/yams_arm.json delete mode 100644 src/main/deploy/black_robot/subsystems/example/yams_elevator.json delete mode 100644 src/main/deploy/black_robot/subsystems/example/yams_pivot.json create mode 100644 src/main/deploy/black_robot/subsystems/lowerflywheel.json create mode 100644 src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json create mode 100644 src/main/deploy/black_robot/subsystems/upperflywheel.json create mode 100644 src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json delete mode 100644 src/main/deploy/buttercup_swerve/subsystems/example.json delete mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json delete mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json delete mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json delete mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json delete mode 100644 src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json create mode 100644 src/main/java/frc/robot/blackteam/LowerFlyWheel.java delete mode 100644 src/main/java/frc/robot/blackteam/ShooterSubsystem.java create mode 100644 src/main/java/frc/robot/blackteam/UpperFlyWheel.java diff --git a/src/main/deploy/black_robot/subsystems/example/percent_motor.json b/src/main/deploy/black_robot/subsystems/example/percent_motor.json deleted file mode 100644 index 027c231f..00000000 --- a/src/main/deploy/black_robot/subsystems/example/percent_motor.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "percent_motor", - "controller": "spark", - "type": "Neo", - "id": 11, - "gearing": 1.0, - "momentOfInertiaKgMSq": 1.0, - "x": 0.5, - "y": 0.0, - "z": 0.25 -} diff --git a/src/main/deploy/black_robot/subsystems/example/velocity_motor.json b/src/main/deploy/black_robot/subsystems/example/velocity_motor.json deleted file mode 100644 index 185de2e0..00000000 --- a/src/main/deploy/black_robot/subsystems/example/velocity_motor.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "velocity_motor", - "controller": "spark", - "type": "Neo", - "id": 14, - "gearing": 1.0, - "momentOfInertiaKgMSq": 1.0, - "x": -0.25, - "y": 0.0, - "z": 0.25, - "kP": 0.1, - "kI": 0.0, - "kD": 0.01, - "iZone": 0.0, - "kS": 0.1, - "kV": 0.1, - "kA": 0.0 -} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_arm.json b/src/main/deploy/black_robot/subsystems/example/yams_arm.json deleted file mode 100644 index 38cf9e27..00000000 --- a/src/main/deploy/black_robot/subsystems/example/yams_arm.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "motorSetup": { - "name": "Arm", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 11 - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "length": { - "val": 0.135, - "uom": "in" - }, - "lowerHardLimit": { - "val": -80, - "uom": "deg" - }, - "upperHardLimit": { - "val": 200, - "uom": "deg" - }, - "startingAngle": { - "val": 0, - "uom": "deg" - }, - "lowerSoftLimit": { - "val": -30, - "uom": "deg" - }, - "upperSoftLimit": { - "val": 100, - "uom": "deg" - }, - "gearing": [ - 3, - 4 - ], - "mass": { - "val": 1, - "uom": "lbs" - }, - "horizontalZero": { - "val": 0, - "uom": "deg" - } -} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_elevator.json b/src/main/deploy/black_robot/subsystems/example/yams_elevator.json deleted file mode 100644 index 55046e05..00000000 --- a/src/main/deploy/black_robot/subsystems/example/yams_elevator.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "motorSetup": { - "name": "Elevator", - "controllerType": "spark", - "motorType": "Neo", - "canId": 10 - }, - "motorSystemId": { - "feedBack": { - "p": 2.7739E-10, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.013799, - "g": 0.24168, - "v": 10.219, - "a": 0.2153 - }, - "maxVelocity": { - "val": 4, - "uom": "m/s" - }, - "maxAcceleration": { - "val": 20, - "uom": "m/s^2" - }, - "controlMode": "OPEN_LOOP" - }, - "sprocketTeeth": 22, - "lowerSoftLimit": { - "val": 0.1, - "uom": "m" - }, - "upperSoftLimit": { - "val": 2, - "uom": "m" - }, - "lowerHardLimit": { - "val": 0.1, - "uom": "m" - }, - "upperHardLimit": { - "val": 3, - "uom": "m" - }, - "gearing": [ - 3, - 4 - ], - "startingPosition": { - "val": 0.5, - "uom": "m" - }, - "mass": { - "val": 16, - "uom": "lbs" - } -} diff --git a/src/main/deploy/black_robot/subsystems/example/yams_pivot.json b/src/main/deploy/black_robot/subsystems/example/yams_pivot.json deleted file mode 100644 index 329453e1..00000000 --- a/src/main/deploy/black_robot/subsystems/example/yams_pivot.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "motorSetup": { - "name": "Turret", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 12 - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "lowerHardLimit": { - "val": -100, - "uom": "deg" - }, - "upperHardLimit": { - "val": 200, - "uom": "deg" - }, - "startingAngle": { - "val": 0, - "uom": "deg" - }, - "lowerSoftLimit": { - "val": -30, - "uom": "deg" - }, - "upperSoftLimit": { - "val": 100, - "uom": "deg" - }, - "gearing": [ - 3, - 4 - ], - "startingPosition": { - "val": 0, - "uom": "deg" - }, - "moi": 0.001 -} diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel.json new file mode 100644 index 00000000..7ce8b5e6 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel.json @@ -0,0 +1,5 @@ +{ + "devices": { + "yams_shooter": "lowerflywheel/flywheel.json" + } +} diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json new file mode 100644 index 00000000..00178849 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json @@ -0,0 +1,49 @@ +{ + "motorSetup": { + "name": "lowerflywheel", + "controllerType": "thrifty", + "motorType": "Neo", + "canId": 10 + }, + "motorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "simMotorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "lowerSoftLimit": { + "val": -10000, + "uom": "rpm" + }, + "upperSoftLimit": { + "val": 10000, + "uom": "rpm" + }, + "stages": "52:24", + "mass": { + "val": 2, + "uom": "kg" + }, + "diameter": { + "val": 4, + "uom": "in" + } +} diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel.json new file mode 100644 index 00000000..9056d051 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/upperflywheel.json @@ -0,0 +1,5 @@ +{ + "devices": { + "yams_shooter": "upperflywheel/flywheel.json" + } +} diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json new file mode 100644 index 00000000..d98ede8c --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json @@ -0,0 +1,49 @@ +{ + "motorSetup": { + "name": "upperflywheel", + "controllerType": "thrifty", + "motorType": "Neo", + "canId": 11 + }, + "motorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "simMotorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.04234, + "v": 0.11449, + "a": 0.094235 + } + }, + "lowerSoftLimit": { + "val": -10000, + "uom": "rpm" + }, + "upperSoftLimit": { + "val": 10000, + "uom": "rpm" + }, + "stages": "52:24", + "mass": { + "val": 2, + "uom": "kg" + }, + "diameter": { + "val": 4, + "uom": "in" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example.json b/src/main/deploy/buttercup_swerve/subsystems/example.json deleted file mode 100644 index 928061d8..00000000 --- a/src/main/deploy/buttercup_swerve/subsystems/example.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "devices": { - "percent_motor": "example/percent_motor.json", - "velocity_motor": "example/velocity_motor.json" - }, - "display": true -} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json b/src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json deleted file mode 100644 index 027c231f..00000000 --- a/src/main/deploy/buttercup_swerve/subsystems/example/percent_motor.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "percent_motor", - "controller": "spark", - "type": "Neo", - "id": 11, - "gearing": 1.0, - "momentOfInertiaKgMSq": 1.0, - "x": 0.5, - "y": 0.0, - "z": 0.25 -} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json b/src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json deleted file mode 100644 index 185de2e0..00000000 --- a/src/main/deploy/buttercup_swerve/subsystems/example/velocity_motor.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "velocity_motor", - "controller": "spark", - "type": "Neo", - "id": 14, - "gearing": 1.0, - "momentOfInertiaKgMSq": 1.0, - "x": -0.25, - "y": 0.0, - "z": 0.25, - "kP": 0.1, - "kI": 0.0, - "kD": 0.01, - "iZone": 0.0, - "kS": 0.1, - "kV": 0.1, - "kA": 0.0 -} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json b/src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json deleted file mode 100644 index 38cf9e27..00000000 --- a/src/main/deploy/buttercup_swerve/subsystems/example/yams_arm.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "motorSetup": { - "name": "Arm", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 11 - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "length": { - "val": 0.135, - "uom": "in" - }, - "lowerHardLimit": { - "val": -80, - "uom": "deg" - }, - "upperHardLimit": { - "val": 200, - "uom": "deg" - }, - "startingAngle": { - "val": 0, - "uom": "deg" - }, - "lowerSoftLimit": { - "val": -30, - "uom": "deg" - }, - "upperSoftLimit": { - "val": 100, - "uom": "deg" - }, - "gearing": [ - 3, - 4 - ], - "mass": { - "val": 1, - "uom": "lbs" - }, - "horizontalZero": { - "val": 0, - "uom": "deg" - } -} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json b/src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json deleted file mode 100644 index 55046e05..00000000 --- a/src/main/deploy/buttercup_swerve/subsystems/example/yams_elevator.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "motorSetup": { - "name": "Elevator", - "controllerType": "spark", - "motorType": "Neo", - "canId": 10 - }, - "motorSystemId": { - "feedBack": { - "p": 2.7739E-10, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.013799, - "g": 0.24168, - "v": 10.219, - "a": 0.2153 - }, - "maxVelocity": { - "val": 4, - "uom": "m/s" - }, - "maxAcceleration": { - "val": 20, - "uom": "m/s^2" - }, - "controlMode": "OPEN_LOOP" - }, - "sprocketTeeth": 22, - "lowerSoftLimit": { - "val": 0.1, - "uom": "m" - }, - "upperSoftLimit": { - "val": 2, - "uom": "m" - }, - "lowerHardLimit": { - "val": 0.1, - "uom": "m" - }, - "upperHardLimit": { - "val": 3, - "uom": "m" - }, - "gearing": [ - 3, - 4 - ], - "startingPosition": { - "val": 0.5, - "uom": "m" - }, - "mass": { - "val": 16, - "uom": "lbs" - } -} diff --git a/src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json b/src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json deleted file mode 100644 index 329453e1..00000000 --- a/src/main/deploy/buttercup_swerve/subsystems/example/yams_pivot.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "motorSetup": { - "name": "Turret", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 12 - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "lowerHardLimit": { - "val": -100, - "uom": "deg" - }, - "upperHardLimit": { - "val": 200, - "uom": "deg" - }, - "startingAngle": { - "val": 0, - "uom": "deg" - }, - "lowerSoftLimit": { - "val": -30, - "uom": "deg" - }, - "upperSoftLimit": { - "val": 100, - "uom": "deg" - }, - "gearing": [ - 3, - 4 - ], - "startingPosition": { - "val": 0, - "uom": "deg" - }, - "moi": 0.001 -} diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json new file mode 100644 index 00000000..7ce8b5e6 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json @@ -0,0 +1,5 @@ +{ + "devices": { + "yams_shooter": "lowerflywheel/flywheel.json" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json new file mode 100644 index 00000000..00178849 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json @@ -0,0 +1,49 @@ +{ + "motorSetup": { + "name": "lowerflywheel", + "controllerType": "thrifty", + "motorType": "Neo", + "canId": 10 + }, + "motorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "simMotorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "lowerSoftLimit": { + "val": -10000, + "uom": "rpm" + }, + "upperSoftLimit": { + "val": 10000, + "uom": "rpm" + }, + "stages": "52:24", + "mass": { + "val": 2, + "uom": "kg" + }, + "diameter": { + "val": 4, + "uom": "in" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json new file mode 100644 index 00000000..9056d051 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json @@ -0,0 +1,5 @@ +{ + "devices": { + "yams_shooter": "upperflywheel/flywheel.json" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json new file mode 100644 index 00000000..d98ede8c --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json @@ -0,0 +1,49 @@ +{ + "motorSetup": { + "name": "upperflywheel", + "controllerType": "thrifty", + "motorType": "Neo", + "canId": 11 + }, + "motorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "simMotorSystemId": { + "feedBack": { + "p": 0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.04234, + "v": 0.11449, + "a": 0.094235 + } + }, + "lowerSoftLimit": { + "val": -10000, + "uom": "rpm" + }, + "upperSoftLimit": { + "val": 10000, + "uom": "rpm" + }, + "stages": "52:24", + "mass": { + "val": 2, + "uom": "kg" + }, + "diameter": { + "val": 4, + "uom": "in" + } +} diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index f82ff524..81deb748 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 98; - public static final String GIT_SHA = "6e54f4359964e736d432c75a01fe96cd0ed98220"; - public static final String GIT_DATE = "2025-12-01 17:56:33 EST"; + public static final int GIT_REVISION = 99; + public static final String GIT_SHA = "499f035da6e24f57b41830afe1d9f57d3859da18"; + public static final String GIT_DATE = "2025-12-01 22:04:28 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-01 22:01:24 EST"; - public static final long BUILD_UNIX_TIME = 1764644484547L; + public static final String BUILD_DATE = "2025-12-02 19:29:18 EST"; + public static final long BUILD_UNIX_TIME = 1764721758279L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 9fb70753..3a3c9b92 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -14,13 +14,15 @@ public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; - private ShooterSubsystem shooterSubsystem; + private LowerFlyWheel lowerFlyWheel; + private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); public BlackRobot(String directory) { super(directory); drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); - shooterSubsystem = new ShooterSubsystem(); + lowerFlyWheel = new LowerFlyWheel(); + upperFlyWheel = new UpperFlyWheel(); // NamedCommands.registerCommand("shoot", launchToDistance(20)); } @@ -38,36 +40,37 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05)); driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05)); - driver.createLeftBumper().whileTrue(shooterSubsystem.setVelocity(RPM.of(60))); + driver.createLeftBumper().whileTrue(lowerFlyWheel.setVelocity(RPM.of(60))); - driver.createXButton().whileTrue(shooterSubsystem.set(0.3)); - driver.createYButton().whileTrue(shooterSubsystem.set(-0.3)); + driver.createXButton().whileTrue(lowerFlyWheel.set(0.3).alongWith(upperFlyWheel.set(-0.3))); + driver.createYButton().whileTrue(lowerFlyWheel.set(-0.3).alongWith(upperFlyWheel.set(0.3))); - driver.createBButton().whileTrue(shooterSubsystem.systemID()); + driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); + driver.createStartButton().whileTrue(upperFlyWheel.systemID()); driver.createAButton().whileTrue(feeder.setSpeed(0.5)); JoystickButton rightBumper = driver.createRightBumper(); State prep = flyWheelStateMachine.addState( - "prep", Commands.print("PREP").andThen(shooterSubsystem.setVelocity(RPM.of(1000)))); + "prep", Commands.print("PREP").andThen(lowerFlyWheel.setVelocity(RPM.of(1000)))); State fire = flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(feeder.setSpeed(0.5))); flyWheelStateMachine.setInitialState(prep); - prep.switchTo(fire).when(shooterSubsystem.isNearTarget(RPM.of(200))); + prep.switchTo(fire).when(lowerFlyWheel.isNearTarget(RPM.of(200))); rightBumper.whileTrue(flyWheelStateMachine); rightBumper.onFalse( - shooterSubsystem + lowerFlyWheel .setVelocity(RPM.of(0)) - .andThen(shooterSubsystem.set(0)) + .andThen(lowerFlyWheel.set(0)) .alongWith(feeder.setSpeed(0))); } @Override public void setupDefaultCommands(Controller driver, Controller operator) { - shooterSubsystem.setDefaultCommand( - shooterSubsystem.joyStickControl(() -> driver.getRightTrigger())); + lowerFlyWheel.setDefaultCommand(lowerFlyWheel.joyStickControl(() -> driver.getRightTrigger())); + upperFlyWheel.setDefaultCommand(upperFlyWheel.joyStickControl(() -> driver.getLeftTrigger())); drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); feeder.setDefaultCommand(feeder.joyStickControl(() -> driver.getLeftTrigger())); } diff --git a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java new file mode 100644 index 00000000..b74d51c0 --- /dev/null +++ b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java @@ -0,0 +1,88 @@ +package frc.robot.blackteam; + +import static edu.wpi.first.units.Units.RPM; +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 edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj2.command.Command; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; +import yams.mechanisms.velocity.FlyWheel; + +public class LowerFlyWheel extends GenericSubsystem { + private AngularVelocity setpoint = RPM.of(0); + + private InterpolatingDoubleTreeMap distanceToVelocityMap = + InterpolatingDoubleTreeMap.ofEntries( + Map.entry(0.0, 0.0), + Map.entry(0.5, 500.0), + Map.entry(1.0, 1000.0), + Map.entry(1.5, 1500.0)); + + private FlyWheel lowerFlyWheel; + + /** Creates a new Shooter. */ + public LowerFlyWheel() { + super("lowerflywheel.json"); + lowerFlyWheel = (FlyWheel) devices.get("lowerflywheel"); + } + + public Command setLowerFlyWheelSpeed(double speed) { + return lowerFlyWheel.set(speed); + } + + /** + * @param dutyCycle DutyCycle to set. + * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} + */ + public Command set(double dutyCycle) { + return lowerFlyWheel.set(dutyCycle); + } + + // TODO: fix this to use Supplier + public Command launchToDistance(DoubleSupplier distanceSupplier) { + return lowerFlyWheel.setSpeed( + () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); + } + + public Command spinAtSpeed(Supplier speedSupplier) { + return lowerFlyWheel.setSpeed(speedSupplier); + } + + public Command joyStickControl(Supplier speedSupplier) { + return lowerFlyWheel.set(speedSupplier); + } + + public Supplier getVelocity() { + return () -> lowerFlyWheel.getSpeed(); + } + + public BooleanSupplier isNearTarget(AngularVelocity range) { + return lowerFlyWheel.isNear(setpoint, range); + } + + @Override + public void periodic() { + lowerFlyWheel.updateTelemetry(); + } + + @Override + public void simulationPeriodic() { + lowerFlyWheel.simIterate(); + } + + public Command setVelocity(AngularVelocity speed) { + setpoint = speed; + return lowerFlyWheel.setSpeed(speed); + } + + public Command systemID() { + return lowerFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); + } +} diff --git a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java b/src/main/java/frc/robot/blackteam/ShooterSubsystem.java deleted file mode 100644 index d9ef347a..00000000 --- a/src/main/java/frc/robot/blackteam/ShooterSubsystem.java +++ /dev/null @@ -1,161 +0,0 @@ -package frc.robot.blackteam; - -import static edu.wpi.first.units.Units.Amps; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.Pounds; -import static edu.wpi.first.units.Units.RPM; -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 com.thethriftybot.ThriftyNova; -import edu.wpi.first.math.controller.SimpleMotorFeedforward; -import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj2.command.Command; -import java.util.Map; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; -import org.frc5010.common.arch.GenericSubsystem; -import yams.gearing.GearBox; -import yams.gearing.MechanismGearing; -import yams.mechanisms.config.FlyWheelConfig; -import yams.mechanisms.velocity.FlyWheel; -import yams.motorcontrollers.SmartMotorController; -import yams.motorcontrollers.SmartMotorControllerConfig; -import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; -import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; -import yams.motorcontrollers.local.NovaWrapper; - -public class ShooterSubsystem extends GenericSubsystem { - private final ThriftyNova lowerMotor = new ThriftyNova(10); - private final ThriftyNova upperMotor = new ThriftyNova(11); - private AngularVelocity setpoint = RPM.of(0); - - private InterpolatingDoubleTreeMap distanceToVelocityMap = - InterpolatingDoubleTreeMap.ofEntries( - Map.entry(0.0, 0.0), - Map.entry(0.5, 500.0), - Map.entry(1.0, 1000.0), - Map.entry(1.5, 1500.0)); - - private final SmartMotorControllerConfig lowerMotorConfig = - new SmartMotorControllerConfig(this) - .withClosedLoopController(0.0, 0, 0) - .withSimClosedLoopController(0.0, 0, 0) - .withGearing(new MechanismGearing(GearBox.fromStages("52:24"))) - .withIdleMode(MotorMode.BRAKE) - .withTelemetry("LowerShooterMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withClosedLoopRampRate(Seconds.of(0.25)) - // ThriftyNova does not support separate closed loop and open loop ramp rates - // .withOpenLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new SimpleMotorFeedforward(0.04234, 0.11449, 0.094235)) - .withSimFeedforward(new SimpleMotorFeedforward(0.04234, 0.11449, 0.094235)) - .withControlMode(ControlMode.CLOSED_LOOP); - private final SmartMotorControllerConfig upperMotorConfig = - new SmartMotorControllerConfig(this) - .withClosedLoopController(0.0, 0, 0) - .withSimClosedLoopController(0.0, 0, 0) - .withGearing(new MechanismGearing(GearBox.fromStages("52:24"))) - .withIdleMode(MotorMode.BRAKE) - .withTelemetry("UpperShooterMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withClosedLoopRampRate(Seconds.of(0.25)) - // ThriftyNova does not support separate closed loop and open loop ramp rates - // .withOpenLoopRampRate(Seconds.of(0.25)) - .withFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) - .withSimFeedforward(new SimpleMotorFeedforward(0.27937, 0.089836, 0.014557)) - .withControlMode(ControlMode.CLOSED_LOOP); - - private final SmartMotorController lowerMotorController = - new NovaWrapper(lowerMotor, DCMotor.getNEO(1), lowerMotorConfig); - // new SparkWrapper(motor, DCMotor.getNEO(1), motorConfig); - private final SmartMotorController upperMotorController = - new NovaWrapper(upperMotor, DCMotor.getNEO(1), upperMotorConfig); - - private final FlyWheelConfig lFlyWheelConfig = - new FlyWheelConfig(lowerMotorController) - .withDiameter(Inches.of(4)) - .withMass(Pounds.of(2)) - .withSoftLimit(RPM.of(-10000), RPM.of(10000)) - .withSpeedometerSimulation(RPM.of(12000)) - .withTelemetry("LowerShooterMech", TelemetryVerbosity.HIGH); - - private FlyWheel lowerFlyWheel = new FlyWheel(lFlyWheelConfig); - - private final FlyWheelConfig uFlyWheelConfig = - new FlyWheelConfig(upperMotorController) - .withDiameter(Inches.of(3)) - .withMass(Pounds.of(2)) - .withSoftLimit(RPM.of(-10000), RPM.of(10000)) - .withSpeedometerSimulation(RPM.of(12000)) - .withTelemetry("UpperShooterMech", TelemetryVerbosity.HIGH); - - private FlyWheel upperFlyWheel = new FlyWheel(uFlyWheelConfig); - - /** Creates a new Shooter. */ - public ShooterSubsystem() {} - - public Command setLowerFLyWheelSpeed(double speed) { - return lowerFlyWheel.set(speed); - } - - public Command setUpperFLyWheelSpeed(double speed) { - return upperFlyWheel.set(speed); - } - - /** - * @param dutyCycle DutyCycle to set. - * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} - */ - public Command set(double dutyCycle) { - return lowerFlyWheel.set(dutyCycle).alongWith(upperFlyWheel.set(-dutyCycle).asProxy()); - } - - // TODO: fix this to use Supplier - public Command launchToDistance(DoubleSupplier distanceSupplier) { - return lowerFlyWheel.setSpeed( - () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); - } - - public Command spinAtSpeed(Supplier speedSupplier) { - return lowerFlyWheel.setSpeed(speedSupplier).alongWith(upperFlyWheel.setSpeed(speedSupplier)); - } - - public Command joyStickControl(Supplier speedSupplier) { - return lowerFlyWheel.set(speedSupplier).alongWith(upperFlyWheel.set(speedSupplier).asProxy()); - } - - public Supplier getVelocity() { - return () -> lowerFlyWheel.getSpeed(); - } - - public BooleanSupplier isNearTarget(AngularVelocity range) { - return lowerFlyWheel.isNear(setpoint, range); - } - - @Override - public void periodic() { - lowerFlyWheel.updateTelemetry(); - } - - @Override - public void simulationPeriodic() { - lowerFlyWheel.simIterate(); - } - - public Command setVelocity(AngularVelocity speed) { - setpoint = speed; - return lowerFlyWheel.setSpeed(speed); - } - - public Command systemID() { - return lowerFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); - } -} diff --git a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java new file mode 100644 index 00000000..1da3b282 --- /dev/null +++ b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java @@ -0,0 +1,88 @@ +package frc.robot.blackteam; + +import static edu.wpi.first.units.Units.RPM; +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 edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj2.command.Command; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; +import yams.mechanisms.velocity.FlyWheel; + +public class UpperFlyWheel extends GenericSubsystem { + private AngularVelocity setpoint = RPM.of(0); + + private InterpolatingDoubleTreeMap distanceToVelocityMap = + InterpolatingDoubleTreeMap.ofEntries( + Map.entry(0.0, 0.0), + Map.entry(0.5, 500.0), + Map.entry(1.0, 1000.0), + Map.entry(1.5, 1500.0)); + + private FlyWheel upperFlyWheel; + + /** Creates a new Shooter. */ + public UpperFlyWheel() { + super("upperflywheel.json"); + upperFlyWheel = (FlyWheel) devices.get("upperflywheel"); + } + + public Command setUpperFLyWheelSpeed(double speed) { + return upperFlyWheel.set(speed); + } + + /** + * @param dutyCycle DutyCycle to set. + * @return {@link edu.wpi.first.wpilibj2.command.RunCommand} + */ + public Command set(double dutyCycle) { + return upperFlyWheel.set(dutyCycle); + } + + // TODO: fix this to use Supplier + public Command launchToDistance(DoubleSupplier distanceSupplier) { + return upperFlyWheel.setSpeed( + () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); + } + + public Command spinAtSpeed(Supplier speedSupplier) { + return upperFlyWheel.setSpeed(speedSupplier); + } + + public Command joyStickControl(Supplier speedSupplier) { + return upperFlyWheel.set(speedSupplier); + } + + public Supplier getVelocity() { + return () -> upperFlyWheel.getSpeed(); + } + + public BooleanSupplier isNearTarget(AngularVelocity range) { + return upperFlyWheel.isNear(setpoint, range); + } + + @Override + public void periodic() { + upperFlyWheel.updateTelemetry(); + } + + @Override + public void simulationPeriodic() { + upperFlyWheel.simIterate(); + } + + public Command setVelocity(AngularVelocity speed) { + setpoint = speed; + return upperFlyWheel.setSpeed(speed); + } + + public Command systemID() { + return upperFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); + } +} diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java index 41b1b176..f57ac3cc 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java @@ -24,9 +24,11 @@ public class YamsShooterConfigurationJson implements DeviceConfiguration { public MotorSetupJson motorSetup = new MotorSetupJson(); public MotorSystemIdJson motorSystemId = new MotorSystemIdJson(); + public MotorSystemIdJson simMotorSystemId = new MotorSystemIdJson(); public UnitValueJson lowerSoftLimit = new UnitValueJson(0, UnitsParser.DEGPS); public UnitValueJson upperSoftLimit = new UnitValueJson(0, UnitsParser.DEGPS); - public double[] gearing; + public double[] gearing = new double[0]; + public String stages = ""; public UnitValueJson voltageCompensation = new UnitValueJson(12, UnitsParser.VOLTS); public UnitValueJson mass = new UnitValueJson(0, UnitsParser.LBS); public UnitValueJson diameter = new UnitValueJson(0, UnitsParser.IN); @@ -44,28 +46,51 @@ public FlyWheel configure(SubsystemBase deviceHandler) { DeviceConfigReader.getMotor( motorSetup.controllerType, motorSetup.motorType, motorSetup.canId); - SmartMotorControllerConfig motorConfig = - new SmartMotorControllerConfig(deviceHandler) - .withClosedLoopController( - motorSystemId.feedBack.p, - motorSystemId.feedBack.i, - motorSystemId.feedBack.d, - UnitsParser.parseAngularVelocity(motorSystemId.maxVelocity), - UnitsParser.parseAngularAcceleration(motorSystemId.maxAcceleration)) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(gearing))) - .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) - .withTelemetry( - motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) - .withMotorInverted(motorSetup.inverted) - .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)) - .withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)) - .withFeedforward( - new SimpleMotorFeedforward( - motorSystemId.feedForward.s, - motorSystemId.feedForward.v, - motorSystemId.feedForward.a)) - .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); + SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(deviceHandler); + if (motorSystemId.maxAcceleration.val > 0) { + motorConfig.withClosedLoopController( + motorSystemId.feedBack.p, + motorSystemId.feedBack.i, + motorSystemId.feedBack.d, + UnitsParser.parseAngularVelocity(motorSystemId.maxVelocity), + UnitsParser.parseAngularAcceleration(motorSystemId.maxAcceleration)); + } else { + motorConfig.withClosedLoopController( + motorSystemId.feedBack.p, motorSystemId.feedBack.i, motorSystemId.feedBack.d); + } + if (simMotorSystemId.maxAcceleration.val > 0) { + motorConfig.withSimClosedLoopController( + simMotorSystemId.feedBack.p, + simMotorSystemId.feedBack.i, + simMotorSystemId.feedBack.d, + UnitsParser.parseAngularVelocity(simMotorSystemId.maxVelocity), + UnitsParser.parseAngularAcceleration(simMotorSystemId.maxAcceleration)); + } else { + motorConfig.withSimClosedLoopController( + simMotorSystemId.feedBack.p, simMotorSystemId.feedBack.i, simMotorSystemId.feedBack.d); + } + if (gearing.length > 0) { + motorConfig.withGearing(new MechanismGearing(GearBox.fromReductionStages(gearing))); + } + if (!stages.isBlank()) { + motorConfig.withGearing(new MechanismGearing(GearBox.fromStages(stages))); + } + motorConfig + .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) + .withTelemetry(motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) + .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) + .withMotorInverted(motorSetup.inverted) + .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)); + if (!motorSetup.controllerType.equals("thrifty")) { + motorConfig.withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)); + } + motorConfig + .withFeedforward( + new SimpleMotorFeedforward( + motorSystemId.feedForward.s, + motorSystemId.feedForward.v, + motorSystemId.feedForward.a)) + .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); MotorSetupJson.setupFollowers(motorConfig, motorSetup); motor.setMotorSimulationType( motor.getMotorConfig().getMotorSimulationType(motorSetup.numberOfMotors)); diff --git a/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java b/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java index c7d180e1..b1b7b02a 100644 --- a/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java +++ b/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java @@ -24,6 +24,7 @@ import org.frc5010.common.sensors.encoder.ThriftyNovaEncoder; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.local.NovaWrapper; /** Add your docs here. */ public class GenericThriftyNovaMotor implements GenericMotorController { @@ -282,7 +283,6 @@ public Motor getMotorConfig() { @Override public SmartMotorController getSmartMotorController(SmartMotorControllerConfig config) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getSmartMotorController'"); + return new NovaWrapper(motor, motorSim, config); } } From b32df9ca15116677aab6124d58732ae034753a40 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Wed, 3 Dec 2025 16:21:47 -0500 Subject: [PATCH 38/93] Did the TODO in UpperFlyWheel and LowerFlyWheel --- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/LowerFlyWheel.java | 7 ++----- src/main/java/frc/robot/blackteam/UpperFlyWheel.java | 7 ++----- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 81deb748..80d4db61 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 99; - public static final String GIT_SHA = "499f035da6e24f57b41830afe1d9f57d3859da18"; - public static final String GIT_DATE = "2025-12-01 22:04:28 EST"; + public static final int GIT_REVISION = 100; + public static final String GIT_SHA = "8fca1fa0b5d50fed60b5f329950c817e274d21c9"; + public static final String GIT_DATE = "2025-12-02 19:33:57 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-02 19:29:18 EST"; - public static final long BUILD_UNIX_TIME = 1764721758279L; + public static final String BUILD_DATE = "2025-12-03 16:18:48 EST"; + public static final long BUILD_UNIX_TIME = 1764796728990L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java index b74d51c0..ddb3dc76 100644 --- a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java @@ -10,7 +10,6 @@ import edu.wpi.first.wpilibj2.command.Command; import java.util.Map; import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.velocity.FlyWheel; @@ -45,10 +44,8 @@ public Command set(double dutyCycle) { return lowerFlyWheel.set(dutyCycle); } - // TODO: fix this to use Supplier - public Command launchToDistance(DoubleSupplier distanceSupplier) { - return lowerFlyWheel.setSpeed( - () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); + public Command launchToDistance(Supplier distanceSupplier) { + return lowerFlyWheel.setSpeed(distanceSupplier); } public Command spinAtSpeed(Supplier speedSupplier) { diff --git a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java index 1da3b282..5d7a0dae 100644 --- a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java @@ -10,7 +10,6 @@ import edu.wpi.first.wpilibj2.command.Command; import java.util.Map; import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.velocity.FlyWheel; @@ -45,10 +44,8 @@ public Command set(double dutyCycle) { return upperFlyWheel.set(dutyCycle); } - // TODO: fix this to use Supplier - public Command launchToDistance(DoubleSupplier distanceSupplier) { - return upperFlyWheel.setSpeed( - () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); + public Command launchToDistance(Supplier distanceSupplier) { + return upperFlyWheel.setSpeed(distanceSupplier); } public Command spinAtSpeed(Supplier speedSupplier) { From b5567b9c66fd6e9fe74adfd847269756f8b2c7da Mon Sep 17 00:00:00 2001 From: pkatzfey Date: Wed, 3 Dec 2025 18:47:19 -0500 Subject: [PATCH 39/93] Updated Shooter --- simgui.json | 12 +++++++- .../controllers/axis/driver_left_trigger.json | 2 +- .../axis/driver_right_trigger.json | 2 +- .../controllers/axis/driver_left_trigger.json | 6 ++-- .../axis/driver_right_trigger.json | 6 ++-- .../subsystems/lowerflywheel/flywheel.json | 3 +- .../subsystems/upperflywheel/flywheel.json | 3 +- src/main/java/frc/robot/BuildConstants.java | 10 +++---- .../java/frc/robot/blackteam/BlackRobot.java | 30 +++++++++++++------ .../frc/robot/blackteam/LowerFlyWheel.java | 7 ++--- .../frc/robot/blackteam/UpperFlyWheel.java | 7 ++--- 11 files changed, 53 insertions(+), 35 deletions(-) diff --git a/simgui.json b/simgui.json index de3514a4..2f0b1142 100644 --- a/simgui.json +++ b/simgui.json @@ -126,6 +126,14 @@ "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/Up": "Command", "/SmartDashboard/Mechanisms/UpperShooterMech/UpperShooterMotor/Commands/ZeroEncoder": "Command", + "/SmartDashboard/Mechanisms/lowerflywheel/lowerflywheelMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/lowerflywheel/lowerflywheelMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/lowerflywheel/lowerflywheelMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/lowerflywheel/lowerflywheelMotor/Commands/ZeroEncoder": "Command", + "/SmartDashboard/Mechanisms/upperflywheel/upperflywheelMotor/Commands/Down": "Command", + "/SmartDashboard/Mechanisms/upperflywheel/upperflywheelMotor/Commands/LiveTuning": "Command", + "/SmartDashboard/Mechanisms/upperflywheel/upperflywheelMotor/Commands/Up": "Command", + "/SmartDashboard/Mechanisms/upperflywheel/upperflywheelMotor/Commands/ZeroEncoder": "Command", "/SmartDashboard/Motors": "Alerts", "/SmartDashboard/PathPlanner": "Alerts", "/SmartDashboard/PhotonAlerts": "Alerts", @@ -141,8 +149,10 @@ "/SmartDashboard/UpperShooterMech/mechanism": "Mechanism2d", "/SmartDashboard/VisionSystemSim-Vision/Sim Field": "Field2d", "/SmartDashboard/VisionSystemSim-main/Sim Field": "Field2d", + "/SmartDashboard/lowerflywheel/mechanism": "Mechanism2d", "/SmartDashboard/navX-Sensor[1]": "Gyro", - "/SmartDashboard/navX-Sensor[4]": "Gyro" + "/SmartDashboard/navX-Sensor[4]": "Gyro", + "/SmartDashboard/upperflywheel/mechanism": "Mechanism2d" }, "windows": { "/FMSInfo": { diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json index 41b79133..f899fd4b 100644 --- a/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json @@ -1,7 +1,7 @@ { "channel": 2, "deadband": 0.07, - "invert": true, + "invert": false, "scale": 1.0, "curvePower": 3, "limit": 1.0 diff --git a/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json index 47b46917..bd2ec447 100644 --- a/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json +++ b/src/main/deploy/black_robot/controllers/axis/driver_right_trigger.json @@ -1,7 +1,7 @@ { "channel": 3, "deadband": 0.07, - "invert": true, + "invert": false, "scale": 1.0, "curvePower": 3, "limit": 1.0 diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json index 41b79133..3359502c 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json @@ -1,8 +1,8 @@ { "channel": 2, "deadband": 0.07, - "invert": true, - "scale": 1.0, + "invert": false, + "scale": 0.5, "curvePower": 3, - "limit": 1.0 + "limit": 0.5 } diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json index 47b46917..c84d2264 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json @@ -1,8 +1,8 @@ { "channel": 3, "deadband": 0.07, - "invert": true, - "scale": 1.0, + "invert": false, + "scale": 0.5, "curvePower": 3, - "limit": 1.0 + "limit": 0.5 } diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json index 00178849..aac808c3 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json @@ -3,7 +3,8 @@ "name": "lowerflywheel", "controllerType": "thrifty", "motorType": "Neo", - "canId": 10 + "canId": 10, + "inverted": true }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json index d98ede8c..9966d1c7 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json @@ -3,7 +3,8 @@ "name": "upperflywheel", "controllerType": "thrifty", "motorType": "Neo", - "canId": 11 + "canId": 11, + "inverted": true }, "motorSystemId": { "feedBack": { diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 81deb748..1c3585f7 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 99; - public static final String GIT_SHA = "499f035da6e24f57b41830afe1d9f57d3859da18"; - public static final String GIT_DATE = "2025-12-01 22:04:28 EST"; + public static final int GIT_REVISION = 100; + public static final String GIT_SHA = "8fca1fa0b5d50fed60b5f329950c817e274d21c9"; + public static final String GIT_DATE = "2025-12-02 19:33:57 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-02 19:29:18 EST"; - public static final long BUILD_UNIX_TIME = 1764721758279L; + public static final String BUILD_DATE = "2025-12-03 18:42:45 EST"; + public static final long BUILD_UNIX_TIME = 1764805365448L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 3a3c9b92..bac3e21b 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -17,6 +17,11 @@ public class BlackRobot extends GenericRobot { private LowerFlyWheel lowerFlyWheel; private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); + private final double Speed1 = 0.15; + private final double Speed2 = 0.16; + private final double Speed3 = 0.17; + private final double Speed4 = 0.18; + private final double upperOffSet = 0.01; public BlackRobot(String directory) { super(directory); @@ -37,17 +42,25 @@ private Command launchToDistance(int i) { @Override public void configureButtonBindings(Controller driver, Controller operator) { - driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05)); - driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05)); - - driver.createLeftBumper().whileTrue(lowerFlyWheel.setVelocity(RPM.of(60))); - - driver.createXButton().whileTrue(lowerFlyWheel.set(0.3).alongWith(upperFlyWheel.set(-0.3))); - driver.createYButton().whileTrue(lowerFlyWheel.set(-0.3).alongWith(upperFlyWheel.set(0.3))); + driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.5)); + driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.5)); + + driver + .createAButton() + .whileTrue(lowerFlyWheel.set(Speed1).alongWith(upperFlyWheel.set(Speed1 + upperOffSet))); + driver + .createXButton() + .whileTrue(lowerFlyWheel.set(Speed2).alongWith(upperFlyWheel.set(Speed2 + upperOffSet))); + driver + .createYButton() + .whileTrue(lowerFlyWheel.set(Speed3).alongWith(upperFlyWheel.set(Speed3 + upperOffSet))); + driver + .createBButton() + .whileTrue(lowerFlyWheel.set(Speed4).alongWith(upperFlyWheel.set(Speed4 + upperOffSet))); driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); driver.createStartButton().whileTrue(upperFlyWheel.systemID()); - driver.createAButton().whileTrue(feeder.setSpeed(0.5)); + driver.createLeftBumper().whileTrue(feeder.setSpeed(-0.5)); JoystickButton rightBumper = driver.createRightBumper(); @@ -72,7 +85,6 @@ public void setupDefaultCommands(Controller driver, Controller operator) { lowerFlyWheel.setDefaultCommand(lowerFlyWheel.joyStickControl(() -> driver.getRightTrigger())); upperFlyWheel.setDefaultCommand(upperFlyWheel.joyStickControl(() -> driver.getLeftTrigger())); drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); - feeder.setDefaultCommand(feeder.joyStickControl(() -> driver.getLeftTrigger())); } @Override diff --git a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java index b74d51c0..ddb3dc76 100644 --- a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java @@ -10,7 +10,6 @@ import edu.wpi.first.wpilibj2.command.Command; import java.util.Map; import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.velocity.FlyWheel; @@ -45,10 +44,8 @@ public Command set(double dutyCycle) { return lowerFlyWheel.set(dutyCycle); } - // TODO: fix this to use Supplier - public Command launchToDistance(DoubleSupplier distanceSupplier) { - return lowerFlyWheel.setSpeed( - () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); + public Command launchToDistance(Supplier distanceSupplier) { + return lowerFlyWheel.setSpeed(distanceSupplier); } public Command spinAtSpeed(Supplier speedSupplier) { diff --git a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java index 1da3b282..5d7a0dae 100644 --- a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java @@ -10,7 +10,6 @@ import edu.wpi.first.wpilibj2.command.Command; import java.util.Map; import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; import yams.mechanisms.velocity.FlyWheel; @@ -45,10 +44,8 @@ public Command set(double dutyCycle) { return upperFlyWheel.set(dutyCycle); } - // TODO: fix this to use Supplier - public Command launchToDistance(DoubleSupplier distanceSupplier) { - return upperFlyWheel.setSpeed( - () -> RPM.of(distanceToVelocityMap.get(distanceSupplier.getAsDouble()))); + public Command launchToDistance(Supplier distanceSupplier) { + return upperFlyWheel.setSpeed(distanceSupplier); } public Command spinAtSpeed(Supplier speedSupplier) { From 9662cec47142c477ae653456f4a96367b67dcc6d Mon Sep 17 00:00:00 2001 From: pkatzfey Date: Thu, 4 Dec 2025 18:52:02 -0500 Subject: [PATCH 40/93] Updated Motors+LED 12/4 --- simgui.json | 18 ++++++++++++++++++ .../black_robot/subsystems/led_strip.json | 16 ++++++++++++++++ .../buttercup_swerve/subsystems/led_strip.json | 16 ++++++++++++++++ .../yagsl_swerve/modules/backleft.json | 6 +++--- .../yagsl_swerve/modules/backright.json | 6 +++--- .../yagsl_swerve/modules/frontleft.json | 6 +++--- .../yagsl_swerve/modules/frontright.json | 6 +++--- .../yagsl_swerve/modules/pidfproperties.json | 4 ++-- src/main/deploy/robots.json | 2 +- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- .../java/frc/robot/blackteam/BlackRobot.java | 5 +++-- .../frc/robot/blackteam/LowerFlyWheel.java | 2 +- .../frc/robot/blackteam/UpperFlyWheel.java | 2 +- 13 files changed, 75 insertions(+), 24 deletions(-) create mode 100644 src/main/deploy/black_robot/subsystems/led_strip.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/led_strip.json diff --git a/simgui.json b/simgui.json index 2f0b1142..98fba3fc 100644 --- a/simgui.json +++ b/simgui.json @@ -1,5 +1,13 @@ { "HALProvider": { + "Addressable LEDs": { + "0": { + "columns": 15 + }, + "window": { + "visible": true + } + }, "Other Devices": { "SPARK MAX [12]": { "header": { @@ -741,6 +749,16 @@ "window": { "visible": true } + }, + "/SmartDashboard/lowerflywheel/mechanism": { + "window": { + "visible": true + } + }, + "/SmartDashboard/upperflywheel/mechanism": { + "window": { + "visible": true + } } } }, diff --git a/src/main/deploy/black_robot/subsystems/led_strip.json b/src/main/deploy/black_robot/subsystems/led_strip.json new file mode 100644 index 00000000..25c307af --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/led_strip.json @@ -0,0 +1,16 @@ +{ + "length": 30, + "dataPin": 0, + "segments": [ + { + "name": "status_indicator", + "start": 0, + "end": 9 + }, + { + "name": "team_colors", + "start": 10, + "end": 29 + } + ] +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/led_strip.json b/src/main/deploy/buttercup_swerve/subsystems/led_strip.json new file mode 100644 index 00000000..25c307af --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/led_strip.json @@ -0,0 +1,16 @@ +{ + "length": 30, + "dataPin": 0, + "segments": [ + { + "name": "status_indicator", + "start": 0, + "end": 9 + }, + { + "name": "team_colors", + "start": 10, + "end": 29 + } + ] +} diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json index dce9bfb1..504e4b29 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 3, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 4, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json index d2d7a285..b58197cc 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 5, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 6, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json index b4f039c9..073845b7 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 1, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 2, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json index 02c73e8d..30d60b2c 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json @@ -1,16 +1,16 @@ { "drive": { - "type": "nova_neo", + "type": "neo", "id": 7, "canbus": null }, "angle": { - "type": "nova_neo", + "type": "neo", "id": 8, "canbus": null }, "encoder": { - "type": "thrifty_nova_redux", + "type": "canandmag", "id": 0, "canbus": null }, diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json index 6ae6eac0..7fd0a90a 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json @@ -1,13 +1,13 @@ { "drive": { - "p": 0.000, + "p": 0.0020645, "i": 0, "d": 0, "f": 0, "iz": 0 }, "angle": { - "p": 0.00003, + "p": 0.01, "i": 0, "d": 0.0000, "f": 0, diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index c2287dfd..31ca09a9 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -2,7 +2,7 @@ "competitionPin": 0, "robots": { "black_robot": { - "id": "black_robot", + "id": "00:80:2F:33:04:33", "robotClass": "frc.robot.blackteam.BlackRobot", "simulate": true, "competition": true diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 1c3585f7..b19c5352 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 100; - public static final String GIT_SHA = "8fca1fa0b5d50fed60b5f329950c817e274d21c9"; - public static final String GIT_DATE = "2025-12-02 19:33:57 EST"; + public static final int GIT_REVISION = 103; + public static final String GIT_SHA = "f40a92268584672a6c4891e68e692529e42ce4c9"; + public static final String GIT_DATE = "2025-12-03 18:50:00 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-03 18:42:45 EST"; - public static final long BUILD_UNIX_TIME = 1764805365448L; + public static final String BUILD_DATE = "2025-12-04 18:38:31 EST"; + public static final long BUILD_UNIX_TIME = 1764891511972L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index bac3e21b..3de7270e 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -12,6 +12,7 @@ import org.frc5010.common.drive.GenericDrivetrain; import org.frc5010.common.sensors.Controller; +// TODO: public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; private LowerFlyWheel lowerFlyWheel; @@ -42,8 +43,8 @@ private Command launchToDistance(int i) { @Override public void configureButtonBindings(Controller driver, Controller operator) { - driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.5)); - driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.5)); + driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); + driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); driver .createAButton() diff --git a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java index ddb3dc76..20bb1c43 100644 --- a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java @@ -80,6 +80,6 @@ public Command setVelocity(AngularVelocity speed) { } public Command systemID() { - return lowerFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); + return lowerFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(10)); } } diff --git a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java index 5d7a0dae..14e14d9c 100644 --- a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java @@ -80,6 +80,6 @@ public Command setVelocity(AngularVelocity speed) { } public Command systemID() { - return upperFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(3)); + return upperFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(10)); } } From 65c7fd05bdcebeb395db896557dd118df809d225 Mon Sep 17 00:00:00 2001 From: pkatzfey Date: Thu, 4 Dec 2025 19:01:59 -0500 Subject: [PATCH 41/93] Revision-Use this 12/4 --- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 10 +++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index b19c5352..37be26fc 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 103; - public static final String GIT_SHA = "f40a92268584672a6c4891e68e692529e42ce4c9"; - public static final String GIT_DATE = "2025-12-03 18:50:00 EST"; + public static final int GIT_REVISION = 104; + public static final String GIT_SHA = "9662cec47142c477ae653456f4a96367b67dcc6d"; + public static final String GIT_DATE = "2025-12-04 18:52:02 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-04 18:38:31 EST"; - public static final long BUILD_UNIX_TIME = 1764891511972L; + public static final String BUILD_DATE = "2025-12-04 18:58:21 EST"; + public static final long BUILD_UNIX_TIME = 1764892701673L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 3de7270e..af757a25 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -2,6 +2,7 @@ import static edu.wpi.first.units.Units.RPM; +import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.button.JoystickButton; @@ -11,6 +12,7 @@ import org.frc5010.common.config.ConfigConstants; import org.frc5010.common.drive.GenericDrivetrain; import org.frc5010.common.sensors.Controller; +import org.frc5010.common.subsystems.LEDStrip; // TODO: public class BlackRobot extends GenericRobot { @@ -48,7 +50,13 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver .createAButton() - .whileTrue(lowerFlyWheel.set(Speed1).alongWith(upperFlyWheel.set(Speed1 + upperOffSet))); + .whileTrue( + Commands.run( + () -> + LEDStrip.changeSegmentPattern( + "status_indicator", LEDStrip.getSolidPattern(Color.kAquamarine))) + .andThen( + lowerFlyWheel.set(Speed1).alongWith(upperFlyWheel.set(Speed1 + upperOffSet)))); driver .createXButton() .whileTrue(lowerFlyWheel.set(Speed2).alongWith(upperFlyWheel.set(Speed2 + upperOffSet))); From a38e84dba465aff86d17c4e628f4e37f8092b510 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Thu, 4 Dec 2025 20:37:30 -0500 Subject: [PATCH 42/93] LEDS and SysID Made LEDs work a better by deactivating the ALL_LEDS segment when in teleop (it overwrites others). Figured out how to get LEDs set with commands. Copied SYSID code into FlyWheels so I could mess with them and figure out their issues. Got output, but still not great performance. --- .SysId/sysid-window.json | 24 ++++-- .../subsystems/lowerflywheel/flywheel.json | 14 ++-- .../subsystems/upperflywheel/flywheel.json | 8 +- src/main/java/frc/robot/BuildConstants.java | 10 +-- .../java/frc/robot/blackteam/BlackRobot.java | 29 ++++++-- .../frc/robot/blackteam/LowerFlyWheel.java | 73 ++++++++++++++++++- .../frc/robot/blackteam/UpperFlyWheel.java | 73 ++++++++++++++++++- .../devices/YamsShooterConfigurationJson.java | 5 +- 8 files changed, 203 insertions(+), 33 deletions(-) diff --git a/.SysId/sysid-window.json b/.SysId/sysid-window.json index bad6833f..8d0d6d46 100644 --- a/.SysId/sysid-window.json +++ b/.SysId/sysid-window.json @@ -15,13 +15,13 @@ "Window": { "###Analyzer": { "Collapsed": "0", - "Pos": "445,24", - "Size": "447,698" + "Pos": "320,25", + "Size": "360,689" }, "###Data Selector": { "Collapsed": "0", "Pos": "5,480", - "Size": "431,250" + "Size": "310,291" }, "###Log Loader": { "Collapsed": "0", @@ -30,8 +30,8 @@ }, "###Program Log": { "Collapsed": "0", - "Pos": "21,745", - "Size": "795,171" + "Pos": "327,701", + "Size": "360,135" }, "Debug##Default": { "Collapsed": "0", @@ -40,8 +40,18 @@ }, "Diagnostic Plots": { "Collapsed": "0", - "Pos": "894,26", - "Size": "1023,929" + "Pos": "685,25", + "Size": "1225,779" + }, + "Exception Caught!": { + "Collapsed": "0", + "Pos": "720,431", + "Size": "480,128" + }, + "Override Units": { + "Collapsed": "0", + "Pos": "481,289", + "Size": "320,144" } } } diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json index 00178849..f13dbf5c 100644 --- a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json @@ -7,14 +7,14 @@ }, "motorSystemId": { "feedBack": { - "p": 0, + "p": 3.0352E-05, "i": 0.0, "d": 0.0 }, "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 + "s": 0.04234, + "v": 0.74358, + "a": 0.098446 } }, "simMotorSystemId": { @@ -24,9 +24,9 @@ "d": 0.0 }, "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 + "s": 0.04234, + "v": 0.11449, + "a": 0.094235 } }, "lowerSoftLimit": { diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json index d98ede8c..c316713e 100644 --- a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json @@ -7,14 +7,14 @@ }, "motorSystemId": { "feedBack": { - "p": 0, + "p": 3.0352E-05, "i": 0.0, "d": 0.0 }, "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 + "s": 0.04234, + "v": 0.74358, + "a": 0.098446 } }, "simMotorSystemId": { diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 37be26fc..e117b80a 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 104; - public static final String GIT_SHA = "9662cec47142c477ae653456f4a96367b67dcc6d"; - public static final String GIT_DATE = "2025-12-04 18:52:02 EST"; + public static final int GIT_REVISION = 105; + public static final String GIT_SHA = "65c7fd05bdcebeb395db896557dd118df809d225"; + public static final String GIT_DATE = "2025-12-04 19:01:59 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-04 18:58:21 EST"; - public static final long BUILD_UNIX_TIME = 1764892701673L; + public static final String BUILD_DATE = "2025-12-04 20:31:42 EST"; + public static final long BUILD_UNIX_TIME = 1764898302772L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index af757a25..b75e3780 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -2,10 +2,12 @@ import static edu.wpi.first.units.Units.RPM; +import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.button.JoystickButton; +import edu.wpi.first.wpilibj2.command.button.Trigger; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.StateMachine; import org.frc5010.common.arch.StateMachine.State; @@ -47,19 +49,29 @@ private Command launchToDistance(int i) { public void configureButtonBindings(Controller driver, Controller operator) { driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); - + LEDStrip.setSegmentActive("status_indicator", true); + new Trigger(DriverStation::isTeleopEnabled) + .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(ConfigConstants.ALL_LEDS, false))); driver .createAButton() .whileTrue( - Commands.run( + lowerFlyWheel + .set(Speed1) + .alongWith(upperFlyWheel.set(Speed1 + upperOffSet)) + .beforeStarting( () -> LEDStrip.changeSegmentPattern( "status_indicator", LEDStrip.getSolidPattern(Color.kAquamarine))) - .andThen( - lowerFlyWheel.set(Speed1).alongWith(upperFlyWheel.set(Speed1 + upperOffSet)))); + .finallyDo( + () -> + LEDStrip.changeSegmentPattern( + "status_indicator", LEDStrip.getSolidPattern(Color.kBlack)))); driver .createXButton() - .whileTrue(lowerFlyWheel.set(Speed2).alongWith(upperFlyWheel.set(Speed2 + upperOffSet))); + .whileTrue( + lowerFlyWheel + .setVelocity(RPM.of(1000)) + .alongWith(upperFlyWheel.setVelocity(RPM.of(1000)))); driver .createYButton() .whileTrue(lowerFlyWheel.set(Speed3).alongWith(upperFlyWheel.set(Speed3 + upperOffSet))); @@ -75,7 +87,12 @@ public void configureButtonBindings(Controller driver, Controller operator) { State prep = flyWheelStateMachine.addState( - "prep", Commands.print("PREP").andThen(lowerFlyWheel.setVelocity(RPM.of(1000)))); + "prep", + Commands.print("PREP") + .andThen( + lowerFlyWheel + .setVelocity(RPM.of(1000)) + .alongWith(upperFlyWheel.setVelocity(RPM.of(1000))))); State fire = flyWheelStateMachine.addState("fire", Commands.print("FIRE").andThen(feeder.setSpeed(0.5))); diff --git a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java index 20bb1c43..c5c0c9d7 100644 --- a/src/main/java/frc/robot/blackteam/LowerFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/LowerFlyWheel.java @@ -6,12 +6,21 @@ import static edu.wpi.first.units.Units.Volts; import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; +import edu.wpi.first.units.VoltageUnit; import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Time; +import edu.wpi.first.units.measure.Velocity; +import edu.wpi.first.units.measure.Voltage; import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Direction; import java.util.Map; import java.util.function.BooleanSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; +import yams.exceptions.FlyWheelConfigurationException; import yams.mechanisms.velocity.FlyWheel; public class LowerFlyWheel extends GenericSubsystem { @@ -80,6 +89,68 @@ public Command setVelocity(AngularVelocity speed) { } public Command systemID() { - return lowerFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(10)); + return sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(7)); + } + + public Command sysId(Voltage maximumVoltage, Velocity step, Time duration) { + SysIdRoutine routine = lowerFlyWheel.getMotor().sysId(maximumVoltage, step, duration); + AngularVelocity max = RPM.of(1000); + AngularVelocity min = RPM.of(-1000); + if (lowerFlyWheel.getShooterConfig().getUpperSoftLimit().isPresent()) { + max = lowerFlyWheel.getShooterConfig().getUpperSoftLimit().get().minus(RPM.of(1)); + } else { + throw new FlyWheelConfigurationException( + "FlyWheel upper hard and motor controller soft limit is empty", + "Cannot create SysIdRoutine.", + "withSoftLimit(Angle,Angle)"); + } + if (lowerFlyWheel.getShooterConfig().getLowerSoftLimit().isPresent()) { + min = lowerFlyWheel.getShooterConfig().getLowerSoftLimit().get().plus(RPM.of(1)); + } else { + throw new FlyWheelConfigurationException( + "FlyWheel lower hard and motor controller soft limit is empty", + "Cannot create SysIdRoutine.", + "withSoftLimit(Angle,Angle)"); + } + Trigger maxTrigger = lowerFlyWheel.gte(max); + Trigger minTrigger = lowerFlyWheel.lte(min); + + Command group = + Commands.print("Starting SysId") + .andThen(Commands.runOnce(lowerFlyWheel.getMotor()::stopClosedLoopController)) + .andThen( + routine + .dynamic(Direction.kForward) + .until(maxTrigger) + .finallyDo( + (interrupted) -> { + if (maxTrigger.getAsBoolean()) System.err.println("Interrupted"); + else System.err.println("Forward done"); + })) + .andThen(Commands.waitSeconds(3)) + .andThen( + routine + .dynamic(Direction.kReverse) + .until(minTrigger) + .finallyDo(() -> System.err.println("Reverse done"))) + .andThen(Commands.waitSeconds(3)) + .andThen( + routine + .quasistatic(Direction.kForward) + .until(maxTrigger) + .finallyDo(() -> System.err.println("Quasistatic forward done"))) + .andThen(Commands.waitSeconds(3)) + .andThen( + routine + .quasistatic(Direction.kReverse) + .until(minTrigger) + .finallyDo(() -> System.err.println("Quasistatic reverse done"))); + + if (lowerFlyWheel.getShooterConfig().getTelemetryName().isPresent()) { + group = group.andThen(Commands.print(getName() + " SysId test done.")); + } + return group + .withName(this.getName() + " SysId") + .finallyDo(lowerFlyWheel.getMotor()::startClosedLoopController); } } diff --git a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java index 14e14d9c..8d95a44b 100644 --- a/src/main/java/frc/robot/blackteam/UpperFlyWheel.java +++ b/src/main/java/frc/robot/blackteam/UpperFlyWheel.java @@ -6,12 +6,21 @@ import static edu.wpi.first.units.Units.Volts; import edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap; +import edu.wpi.first.units.VoltageUnit; import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Time; +import edu.wpi.first.units.measure.Velocity; +import edu.wpi.first.units.measure.Voltage; import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Direction; import java.util.Map; import java.util.function.BooleanSupplier; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; +import yams.exceptions.FlyWheelConfigurationException; import yams.mechanisms.velocity.FlyWheel; public class UpperFlyWheel extends GenericSubsystem { @@ -80,6 +89,68 @@ public Command setVelocity(AngularVelocity speed) { } public Command systemID() { - return upperFlyWheel.sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(10)); + return sysId(Volts.of(12), Volts.of(1).per(Second), Seconds.of(10)); + } + + public Command sysId(Voltage maximumVoltage, Velocity step, Time duration) { + SysIdRoutine routine = upperFlyWheel.getMotor().sysId(maximumVoltage, step, duration); + AngularVelocity max = RPM.of(1000); + AngularVelocity min = RPM.of(-1000); + if (upperFlyWheel.getShooterConfig().getUpperSoftLimit().isPresent()) { + max = upperFlyWheel.getShooterConfig().getUpperSoftLimit().get().minus(RPM.of(1)); + } else { + throw new FlyWheelConfigurationException( + "FlyWheel upper hard and motor controller soft limit is empty", + "Cannot create SysIdRoutine.", + "withSoftLimit(Angle,Angle)"); + } + if (upperFlyWheel.getShooterConfig().getLowerSoftLimit().isPresent()) { + min = upperFlyWheel.getShooterConfig().getLowerSoftLimit().get().plus(RPM.of(1)); + } else { + throw new FlyWheelConfigurationException( + "FlyWheel lower hard and motor controller soft limit is empty", + "Cannot create SysIdRoutine.", + "withSoftLimit(Angle,Angle)"); + } + Trigger maxTrigger = upperFlyWheel.gte(max); + Trigger minTrigger = upperFlyWheel.lte(min); + + Command group = + Commands.print("Starting SysId") + .andThen(Commands.runOnce(upperFlyWheel.getMotor()::stopClosedLoopController)) + .andThen( + routine + .dynamic(Direction.kForward) + .until(maxTrigger) + .finallyDo( + (interrupted) -> { + if (maxTrigger.getAsBoolean()) System.err.println("Interrupted"); + else System.err.println("Forward done"); + })) + .andThen(Commands.waitSeconds(3)) + .andThen( + routine + .dynamic(Direction.kReverse) + .until(minTrigger) + .finallyDo(() -> System.err.println("Reverse done"))) + .andThen(Commands.waitSeconds(3)) + .andThen( + routine + .quasistatic(Direction.kForward) + .until(maxTrigger) + .finallyDo(() -> System.err.println("Quasistatic forward done"))) + .andThen(Commands.waitSeconds(3)) + .andThen( + routine + .quasistatic(Direction.kReverse) + .until(minTrigger) + .finallyDo(() -> System.err.println("Quasistatic reverse done"))); + + if (upperFlyWheel.getShooterConfig().getTelemetryName().isPresent()) { + group = group.andThen(Commands.print(getName() + " SysId test done.")); + } + return group + .withName(this.getName() + " SysId") + .finallyDo(upperFlyWheel.getMotor()::startClosedLoopController); } } diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java index f57ac3cc..dcd8bacd 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java @@ -101,8 +101,9 @@ public FlyWheel configure(SubsystemBase deviceHandler) { // .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) .withDiameter(UnitsParser.parseDistance(diameter)) .withMass(UnitsParser.parseMass(mass)) - .withUpperSoftLimit(UnitsParser.parseAngularVelocity(upperSoftLimit)) - .withLowerSoftLimit(UnitsParser.parseAngularVelocity(lowerSoftLimit)) + .withSoftLimit( + UnitsParser.parseAngularVelocity(lowerSoftLimit), + UnitsParser.parseAngularVelocity(upperSoftLimit)) .withSpeedometerSimulation(UnitsParser.parseAngularVelocity(upperSoftLimit)) .withTelemetry(motorSetup.name, TelemetryVerbosity.valueOf(motorSetup.logLevel)); if (0 != moi) { From 82031633571548aadc739518fbe5e51dacb749b9 Mon Sep 17 00:00:00 2001 From: Curt Date: Thu, 4 Dec 2025 21:47:03 -0500 Subject: [PATCH 43/93] Some changes to how LEDs are ordered LED Segments are ordered after the ALL_LEDS segment so they write last. Segment order can be set in config to change write order. --- .../java/frc/robot/blackteam/BlackRobot.java | 15 ++-- .../json/devices/LEDStripConfigJson.java | 3 +- .../frc5010/common/subsystems/LEDStrip.java | 68 +++++++++++++++++-- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index b75e3780..c3bfb3d9 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -2,6 +2,7 @@ import static edu.wpi.first.units.Units.RPM; +import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; @@ -27,6 +28,8 @@ public class BlackRobot extends GenericRobot { private final double Speed3 = 0.17; private final double Speed4 = 0.18; private final double upperOffSet = 0.01; + private final AngularVelocity velocity2 = RPM.of(1500); + private final String STATUS_LED = "status_indicator"; public BlackRobot(String directory) { super(directory); @@ -49,9 +52,9 @@ private Command launchToDistance(int i) { public void configureButtonBindings(Controller driver, Controller operator) { driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); - LEDStrip.setSegmentActive("status_indicator", true); + LEDStrip.setSegmentActive(STATUS_LED, false); new Trigger(DriverStation::isTeleopEnabled) - .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(ConfigConstants.ALL_LEDS, false))); + .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(STATUS_LED, true))); driver .createAButton() .whileTrue( @@ -61,17 +64,15 @@ public void configureButtonBindings(Controller driver, Controller operator) { .beforeStarting( () -> LEDStrip.changeSegmentPattern( - "status_indicator", LEDStrip.getSolidPattern(Color.kAquamarine))) + STATUS_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) .finallyDo( () -> LEDStrip.changeSegmentPattern( - "status_indicator", LEDStrip.getSolidPattern(Color.kBlack)))); + STATUS_LED, LEDStrip.getSolidPattern(Color.kBlack)))); driver .createXButton() .whileTrue( - lowerFlyWheel - .setVelocity(RPM.of(1000)) - .alongWith(upperFlyWheel.setVelocity(RPM.of(1000)))); + lowerFlyWheel.setVelocity(velocity2).alongWith(upperFlyWheel.setVelocity(velocity2))); driver .createYButton() .whileTrue(lowerFlyWheel.set(Speed3).alongWith(upperFlyWheel.set(Speed3 + upperOffSet))); diff --git a/src/main/java/org/frc5010/common/config/json/devices/LEDStripConfigJson.java b/src/main/java/org/frc5010/common/config/json/devices/LEDStripConfigJson.java index 9f1abbc6..690d0e06 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/LEDStripConfigJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/LEDStripConfigJson.java @@ -15,6 +15,7 @@ public static class SegmentConfigJson { public String name = ""; public int start = 0; public int end = 0; + public int order = 1; } public SegmentConfigJson[] segments = new SegmentConfigJson[0]; @@ -22,7 +23,7 @@ public static class SegmentConfigJson { public void configure() { LEDStrip.createInstance(dataPin, length); for (SegmentConfigJson entry : segments) { - LEDStrip.addSegment(entry.name, entry.start, entry.end); + LEDStrip.addSegment(entry.name, entry.start, entry.end, entry.order); } } } diff --git a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java index eb124335..eeb4f1ba 100644 --- a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java +++ b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java @@ -14,6 +14,7 @@ import edu.wpi.first.wpilibj.LEDPattern; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; import org.frc5010.common.arch.GenericSubsystem; @@ -30,6 +31,7 @@ public static class Segment { public LEDPattern pattern; public AddressableLEDBufferView view; public boolean active = false; + public int order = 0; /** * Creates a new ActivePattern object. @@ -74,6 +76,17 @@ public Segment setActive(boolean active) { return this; } + /** + * Sets the order of the active pattern. + * + * @param order the order to set + * @return the active pattern object + */ + public Segment setOrder(int order) { + this.order = order; + return this; + } + /** * Gets the pattern of the active pattern. * @@ -100,6 +113,15 @@ public AddressableLEDBufferView getView() { public boolean isActive() { return active; } + + /** + * Gets the order of the active pattern. + * + * @return the order of the active pattern + */ + public int getOrder() { + return order; + } } private static final Map segments = new HashMap<>(); @@ -172,11 +194,14 @@ public int getLength() { public Command runPattern() { return run( () -> { - for (Segment ap : segments.values()) { - if (ap.isActive()) { - ap.getPattern().applyTo(ap.getView()); - } - } + segments.values().stream() + .sorted(Comparator.comparingInt(Segment::getOrder)) + .forEach( + ap -> { + if (ap.isActive()) { + ap.getPattern().applyTo(ap.getView()); + } + }); }); } @@ -219,6 +244,14 @@ public static void addSegment(String name, LEDPattern pattern, int startIndex, i segments.put(name, Segment.create().setView(view).setPattern(pattern).setActive(true)); } + /** + * Adds a pattern to the LED strip with the given name, starting at the given index and spanning + * the given length. If the name already corresponds to a pattern, this method does nothing. + * + * @param name The name of the pattern to add + * @param startIndex The starting index of the pattern + * @param length The length of the pattern + */ public static void addSegment(String name, int startIndex, int length) { if (null == instance) { return; @@ -227,6 +260,31 @@ public static void addSegment(String name, int startIndex, int length) { segments.put(name, Segment.create().setView(view).setPattern(LEDPattern.kOff).setActive(false)); } + /** + * Adds a pattern to the LED strip with the given name, starting at the given index and spanning + * the given length. The pattern is active by default. If the name already corresponds to a + * pattern, this method does nothing. + * + * @param name The name of the pattern to add + * @param startIndex The starting index of the pattern + * @param length The length of the pattern + * @param order The order in which the pattern will be displayed. Lower order patterns will be + * displayed before higher order patterns. + */ + public static void addSegment(String name, int startIndex, int length, int order) { + if (null == instance) { + return; + } + AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, length); + segments.put( + name, + Segment.create() + .setView(view) + .setPattern(LEDPattern.kOff) + .setActive(false) + .setOrder(order)); + } + /** * Removes the pattern with the given name from the LED strip. If no pattern with the given name * exists, this method does nothing. From 5d8bb58a47dc55acd932169ddf9ceb45ad531a8d Mon Sep 17 00:00:00 2001 From: pkatzfey Date: Mon, 8 Dec 2025 17:08:49 -0500 Subject: [PATCH 44/93] LED Code To All Other Buttons --- src/main/java/frc/robot/BuildConstants.java | 10 +++---- .../java/frc/robot/blackteam/BlackRobot.java | 27 ++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index e117b80a..d616ffd0 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 105; - public static final String GIT_SHA = "65c7fd05bdcebeb395db896557dd118df809d225"; - public static final String GIT_DATE = "2025-12-04 19:01:59 EST"; + public static final int GIT_REVISION = 107; + public static final String GIT_SHA = "82031633571548aadc739518fbe5e51dacb749b9"; + public static final String GIT_DATE = "2025-12-04 21:47:03 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-04 20:31:42 EST"; - public static final long BUILD_UNIX_TIME = 1764898302772L; + public static final String BUILD_DATE = "2025-12-08 17:07:12 EST"; + public static final long BUILD_UNIX_TIME = 1765231632962L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index c3bfb3d9..db685bbb 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -17,7 +17,6 @@ import org.frc5010.common.sensors.Controller; import org.frc5010.common.subsystems.LEDStrip; -// TODO: public class BlackRobot extends GenericRobot { private GenericDrivetrain drivetrain; private LowerFlyWheel lowerFlyWheel; @@ -72,10 +71,32 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver .createXButton() .whileTrue( - lowerFlyWheel.setVelocity(velocity2).alongWith(upperFlyWheel.setVelocity(velocity2))); + lowerFlyWheel + .set(Speed2) + .alongWith(upperFlyWheel.set(Speed2 + upperOffSet)) + .beforeStarting( + () -> + LEDStrip.changeSegmentPattern( + STATUS_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) + .finallyDo( + () -> + LEDStrip.changeSegmentPattern( + STATUS_LED, LEDStrip.getSolidPattern(Color.kRed)))); + driver .createYButton() - .whileTrue(lowerFlyWheel.set(Speed3).alongWith(upperFlyWheel.set(Speed3 + upperOffSet))); + .whileTrue( + lowerFlyWheel + .set(Speed3) + .alongWith(upperFlyWheel.set(Speed3 + upperOffSet)) + .beforeStarting( + () -> + LEDStrip.changeSegmentPattern( + STATUS_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) + .finallyDo( + () -> + LEDStrip.changeSegmentPattern( + STATUS_LED, LEDStrip.getSolidPattern(Color.kYellowGreen)))); driver .createBButton() .whileTrue(lowerFlyWheel.set(Speed4).alongWith(upperFlyWheel.set(Speed4 + upperOffSet))); From 9e0a8fdd30ba65934bfc6337492626b04262d47d Mon Sep 17 00:00:00 2001 From: Adamtheboss9 Date: Mon, 8 Dec 2025 17:29:31 -0500 Subject: [PATCH 45/93] Added LED changes --- simgui.json | 13 ++------ src/main/java/frc/robot/BuildConstants.java | 10 +++--- .../java/frc/robot/blackteam/BlackRobot.java | 32 ++++++++++++------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/simgui.json b/simgui.json index 98fba3fc..74e9036a 100644 --- a/simgui.json +++ b/simgui.json @@ -163,16 +163,6 @@ "/SmartDashboard/upperflywheel/mechanism": "Mechanism2d" }, "windows": { - "/FMSInfo": { - "window": { - "visible": true - } - }, - "/SmartDashboard/BlackRobot/Auto Modes": { - "window": { - "visible": true - } - }, "/SmartDashboard/DrivePoseEstimator/Pose Field": { "CARPET0": { "image": ".\\pictures\\carpet.png", @@ -861,5 +851,8 @@ }, "NetworkTables Info": { "visible": true + }, + "NetworkTables View": { + "visible": false } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index e117b80a..d18c7c2d 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 105; - public static final String GIT_SHA = "65c7fd05bdcebeb395db896557dd118df809d225"; - public static final String GIT_DATE = "2025-12-04 19:01:59 EST"; + public static final int GIT_REVISION = 107; + public static final String GIT_SHA = "82031633571548aadc739518fbe5e51dacb749b9"; + public static final String GIT_DATE = "2025-12-04 21:47:03 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-04 20:31:42 EST"; - public static final long BUILD_UNIX_TIME = 1764898302772L; + public static final String BUILD_DATE = "2025-12-08 17:28:00 EST"; + public static final long BUILD_UNIX_TIME = 1765232880910L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index c3bfb3d9..7f5f8e0f 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -23,13 +23,14 @@ public class BlackRobot extends GenericRobot { private LowerFlyWheel lowerFlyWheel; private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); - private final double Speed1 = 0.15; - private final double Speed2 = 0.16; - private final double Speed3 = 0.17; - private final double Speed4 = 0.18; - private final double upperOffSet = 0.01; - private final AngularVelocity velocity2 = RPM.of(1500); + private final double SPEED1 = 0.15; + private final double SPEED2 = 0.16; + private final double SPEED3 = 0.17; + private final double SPEED4 = 0.18; + private final double UPPEROFFSET = 0.01; + private final AngularVelocity VELOCITY2 = RPM.of(1500); private final String STATUS_LED = "status_indicator"; + private final String TEAM_COLORS = "team_colors"; public BlackRobot(String directory) { super(directory); @@ -59,8 +60,8 @@ public void configureButtonBindings(Controller driver, Controller operator) { .createAButton() .whileTrue( lowerFlyWheel - .set(Speed1) - .alongWith(upperFlyWheel.set(Speed1 + upperOffSet)) + .set(SPEED1) + .alongWith(upperFlyWheel.set(SPEED1 + UPPEROFFSET)) .beforeStarting( () -> LEDStrip.changeSegmentPattern( @@ -72,13 +73,22 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver .createXButton() .whileTrue( - lowerFlyWheel.setVelocity(velocity2).alongWith(upperFlyWheel.setVelocity(velocity2))); + lowerFlyWheel.setVelocity(VELOCITY2).alongWith(upperFlyWheel.setVelocity(VELOCITY2))); driver .createYButton() - .whileTrue(lowerFlyWheel.set(Speed3).alongWith(upperFlyWheel.set(Speed3 + upperOffSet))); + .whileTrue(lowerFlyWheel.set(SPEED3).alongWith(upperFlyWheel.set(SPEED3 + UPPEROFFSET))); driver .createBButton() - .whileTrue(lowerFlyWheel.set(Speed4).alongWith(upperFlyWheel.set(Speed4 + upperOffSet))); + .whileTrue( + lowerFlyWheel + .set(SPEED4) + .alongWith(upperFlyWheel.set(SPEED4 + UPPEROFFSET)) + .beforeStarting( + () -> { + LEDStrip.setSegmentActive(TEAM_COLORS, true); + LEDStrip.changeSegmentPattern(TEAM_COLORS, LEDStrip.getRainbowPattern(200)); + }) + .finallyDo(() -> LEDStrip.setSegmentActive(TEAM_COLORS, false))); driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); driver.createStartButton().whileTrue(upperFlyWheel.systemID()); From 4bfb29a6260c92c297f887165a51d6bfff80e6cf Mon Sep 17 00:00:00 2001 From: Adamtheboss9 Date: Mon, 8 Dec 2025 18:40:35 -0500 Subject: [PATCH 46/93] More LED changes --- simgui.json | 2 +- .../black_robot/subsystems/led_strip.json | 12 +++- .../subsystems/led_strip.json | 12 +++- src/main/java/frc/robot/BuildConstants.java | 10 +-- .../java/frc/robot/blackteam/BlackRobot.java | 70 +++++++++++-------- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/simgui.json b/simgui.json index 74e9036a..523b98d6 100644 --- a/simgui.json +++ b/simgui.json @@ -2,7 +2,7 @@ "HALProvider": { "Addressable LEDs": { "0": { - "columns": 15 + "columns": 17 }, "window": { "visible": true diff --git a/src/main/deploy/black_robot/subsystems/led_strip.json b/src/main/deploy/black_robot/subsystems/led_strip.json index 25c307af..c414c947 100644 --- a/src/main/deploy/black_robot/subsystems/led_strip.json +++ b/src/main/deploy/black_robot/subsystems/led_strip.json @@ -1,5 +1,5 @@ { - "length": 30, + "length": 34, "dataPin": 0, "segments": [ { @@ -11,6 +11,16 @@ "name": "team_colors", "start": 10, "end": 29 + }, + { + "name": "left_half", + "start": 0, + "end": 16 + }, + { + "name": "right_half", + "start": 17, + "end": 33 } ] } diff --git a/src/main/deploy/buttercup_swerve/subsystems/led_strip.json b/src/main/deploy/buttercup_swerve/subsystems/led_strip.json index 25c307af..c414c947 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/led_strip.json +++ b/src/main/deploy/buttercup_swerve/subsystems/led_strip.json @@ -1,5 +1,5 @@ { - "length": 30, + "length": 34, "dataPin": 0, "segments": [ { @@ -11,6 +11,16 @@ "name": "team_colors", "start": 10, "end": 29 + }, + { + "name": "left_half", + "start": 0, + "end": 16 + }, + { + "name": "right_half", + "start": 17, + "end": 33 } ] } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index d18c7c2d..0eea3d53 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 107; - public static final String GIT_SHA = "82031633571548aadc739518fbe5e51dacb749b9"; - public static final String GIT_DATE = "2025-12-04 21:47:03 EST"; + public static final int GIT_REVISION = 110; + public static final String GIT_SHA = "ec77ba89bf9eb67d7895755d777ffe6e89f78adf"; + public static final String GIT_DATE = "2025-12-08 17:32:18 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-08 17:28:00 EST"; - public static final long BUILD_UNIX_TIME = 1765232880910L; + public static final String BUILD_DATE = "2025-12-08 18:32:07 EST"; + public static final long BUILD_UNIX_TIME = 1765236727004L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 49c3a596..7559c5bb 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -1,6 +1,7 @@ package frc.robot.blackteam; import static edu.wpi.first.units.Units.RPM; +import static edu.wpi.first.units.Units.Seconds; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.wpilibj.DriverStation; @@ -28,8 +29,8 @@ public class BlackRobot extends GenericRobot { private final double SPEED4 = 0.18; private final double UPPEROFFSET = 0.01; private final AngularVelocity VELOCITY2 = RPM.of(1500); - private final String STATUS_LED = "status_indicator"; - private final String TEAM_COLORS = "team_colors"; + private final String FEEDER_LED = "left_half"; + private final String SHOOTER_LED = "right_half"; public BlackRobot(String directory) { super(directory); @@ -52,9 +53,9 @@ private Command launchToDistance(int i) { public void configureButtonBindings(Controller driver, Controller operator) { driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); - LEDStrip.setSegmentActive(STATUS_LED, false); + LEDStrip.setSegmentActive(FEEDER_LED, false); new Trigger(DriverStation::isTeleopEnabled) - .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(STATUS_LED, true))); + .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(FEEDER_LED, true))); driver .createAButton() .whileTrue( @@ -62,32 +63,26 @@ public void configureButtonBindings(Controller driver, Controller operator) { .set(SPEED1) .alongWith(upperFlyWheel.set(SPEED1 + UPPEROFFSET)) .beforeStarting( - () -> - LEDStrip.changeSegmentPattern( - STATUS_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) - .finallyDo( - () -> - LEDStrip.changeSegmentPattern( - STATUS_LED, LEDStrip.getSolidPattern(Color.kBlack)))); + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, true); + LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(50)); + }) + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + driver .createXButton() .whileTrue( - lowerFlyWheel.setVelocity(VELOCITY2).alongWith(upperFlyWheel.setVelocity(VELOCITY2))); - driver - .createYButton() - .whileTrue(lowerFlyWheel.set(SPEED3).alongWith(upperFlyWheel.set(SPEED3 + UPPEROFFSET))); lowerFlyWheel - .set(SPEED2) - .alongWith(upperFlyWheel.set(SPEED3 + UPPEROFFSET)) + .setVelocity(VELOCITY2) + .alongWith(upperFlyWheel.setVelocity(VELOCITY2)) .beforeStarting( () -> LEDStrip.changeSegmentPattern( - STATUS_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) + SHOOTER_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) .finallyDo( () -> LEDStrip.changeSegmentPattern( - STATUS_LED, LEDStrip.getSolidPattern(Color.kRed))); - + SHOOTER_LED, LEDStrip.getSolidPattern(Color.kRed)))); driver .createYButton() .whileTrue( @@ -95,13 +90,11 @@ public void configureButtonBindings(Controller driver, Controller operator) { .set(SPEED3) .alongWith(upperFlyWheel.set(SPEED3 + UPPEROFFSET)) .beforeStarting( - () -> - LEDStrip.changeSegmentPattern( - STATUS_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) - .finallyDo( - () -> - LEDStrip.changeSegmentPattern( - STATUS_LED, LEDStrip.getSolidPattern(Color.kYellowGreen)))); + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, true); + LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); + }) + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver .createBButton() .whileTrue( @@ -110,14 +103,29 @@ public void configureButtonBindings(Controller driver, Controller operator) { .alongWith(upperFlyWheel.set(SPEED4 + UPPEROFFSET)) .beforeStarting( () -> { - LEDStrip.setSegmentActive(TEAM_COLORS, true); - LEDStrip.changeSegmentPattern(TEAM_COLORS, LEDStrip.getRainbowPattern(200)); + LEDStrip.setSegmentActive(SHOOTER_LED, true); + LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(200)); }) - .finallyDo(() -> LEDStrip.setSegmentActive(TEAM_COLORS, false))); + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); driver.createStartButton().whileTrue(upperFlyWheel.systemID()); - driver.createLeftBumper().whileTrue(feeder.setSpeed(-0.5)); + driver + .createLeftBumper() + .whileTrue( + feeder + .setSpeed(-0.5) + .beforeStarting( + () -> { + LEDStrip.setSegmentActive(FEEDER_LED, true); + LEDStrip.changeSegmentPattern( + FEEDER_LED, + LEDStrip.getSolidPattern(Color.kGreen).blink(Seconds.of(.25))); + }) + .finallyDo( + () -> + LEDStrip.changeSegmentPattern( + FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); JoystickButton rightBumper = driver.createRightBumper(); From 6fb710faa0b49882e02a14b29ae0174f642047b0 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Wed, 10 Dec 2025 19:12:44 -0500 Subject: [PATCH 47/93] Final configs for Holiday Party --- .../yagsl_swerve/modules/backleft.json | 6 +++--- .../yagsl_swerve/modules/backright.json | 6 +++--- .../yagsl_swerve/modules/frontleft.json | 8 ++++---- .../yagsl_swerve/modules/frontright.json | 6 +++--- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json index 504e4b29..187c0fa9 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 3, + "id": 6, "canbus": null }, "angle": { "type": "neo", - "id": 4, + "id": 5, "canbus": null }, "encoder": { @@ -19,7 +19,7 @@ "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 253.916, + "absoluteEncoderOffset": 0, "location": { "front": -9.2813, "left": 9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json index b58197cc..7356bcc7 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 5, + "id": 8, "canbus": null }, "angle": { "type": "neo", - "id": 6, + "id": 7, "canbus": null }, "encoder": { @@ -19,7 +19,7 @@ "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 237.832, + "absoluteEncoderOffset": 0, "location": { "front": -9.2813, "left": -9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json index 073845b7..7d857f81 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 1, + "id": 4, "canbus": null }, "angle": { "type": "neo", - "id": 2, + "id": 3, "canbus": null }, "encoder": { @@ -16,10 +16,10 @@ }, "inverted": { "drive": false, - "angle": true + "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 117.773, + "absoluteEncoderOffset": 0, "location": { "front": 9.2813, "left": 9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json index 30d60b2c..571e7b32 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 7, + "id": 2, "canbus": null }, "angle": { "type": "neo", - "id": 8, + "id": 1, "canbus": null }, "encoder": { @@ -19,7 +19,7 @@ "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 333.896, + "absoluteEncoderOffset": 0, "location": { "front": 9.2813, "left": -9.2813 diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 0eea3d53..4b74c687 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 110; - public static final String GIT_SHA = "ec77ba89bf9eb67d7895755d777ffe6e89f78adf"; - public static final String GIT_DATE = "2025-12-08 17:32:18 EST"; + public static final int GIT_REVISION = 111; + public static final String GIT_SHA = "4bfb29a6260c92c297f887165a51d6bfff80e6cf"; + public static final String GIT_DATE = "2025-12-08 18:40:35 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-08 18:32:07 EST"; - public static final long BUILD_UNIX_TIME = 1765236727004L; + public static final String BUILD_DATE = "2025-12-10 18:20:24 EST"; + public static final long BUILD_UNIX_TIME = 1765408824962L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 7559c5bb..9ee9338b 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -23,10 +23,10 @@ public class BlackRobot extends GenericRobot { private LowerFlyWheel lowerFlyWheel; private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); - private final double SPEED1 = 0.15; + private final double SPEED1 = 0.17; private final double SPEED2 = 0.16; - private final double SPEED3 = 0.17; - private final double SPEED4 = 0.18; + private final double SPEED3 = 0.25; + private final double SPEED4 = 0.27; private final double UPPEROFFSET = 0.01; private final AngularVelocity VELOCITY2 = RPM.of(1500); private final String FEEDER_LED = "left_half"; From 1ba57c9f0b1639e1d0c57aac1e125f1cba1cdc3e Mon Sep 17 00:00:00 2001 From: pkatzfey Date: Sun, 11 Jan 2026 13:22:19 -0500 Subject: [PATCH 48/93] reverse intake --- src/main/java/frc/robot/BuildConstants.java | 10 +++--- .../java/frc/robot/blackteam/BlackRobot.java | 31 ++++++++++++++----- vendordeps/yams.json | 4 +-- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 0eea3d53..4a95bb9e 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 110; - public static final String GIT_SHA = "ec77ba89bf9eb67d7895755d777ffe6e89f78adf"; - public static final String GIT_DATE = "2025-12-08 17:32:18 EST"; + public static final int GIT_REVISION = 111; + public static final String GIT_SHA = "4bfb29a6260c92c297f887165a51d6bfff80e6cf"; + public static final String GIT_DATE = "2025-12-08 18:40:35 EST"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2025-12-08 18:32:07 EST"; - public static final long BUILD_UNIX_TIME = 1765236727004L; + public static final String BUILD_DATE = "2026-01-11 13:10:31 EST"; + public static final long BUILD_UNIX_TIME = 1768155031366L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 7559c5bb..18f39cb9 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -8,7 +8,6 @@ import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.JoystickButton; import edu.wpi.first.wpilibj2.command.button.Trigger; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.StateMachine; @@ -127,7 +126,22 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - JoystickButton rightBumper = driver.createRightBumper(); + driver + .createRightBumper() + .whileTrue( + feeder + .setSpeed(0.5) + .beforeStarting( + () -> { + LEDStrip.setSegmentActive(FEEDER_LED, true); + LEDStrip.changeSegmentPattern( + FEEDER_LED, + LEDStrip.getSolidPattern(Color.kGreen).blink(Seconds.of(.25))); + }) + .finallyDo( + () -> + LEDStrip.changeSegmentPattern( + FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); State prep = flyWheelStateMachine.addState( @@ -142,18 +156,19 @@ public void configureButtonBindings(Controller driver, Controller operator) { flyWheelStateMachine.setInitialState(prep); prep.switchTo(fire).when(lowerFlyWheel.isNearTarget(RPM.of(200))); - rightBumper.whileTrue(flyWheelStateMachine); - rightBumper.onFalse( - lowerFlyWheel - .setVelocity(RPM.of(0)) - .andThen(lowerFlyWheel.set(0)) - .alongWith(feeder.setSpeed(0))); + // rightBumper.whileTrue(flyWheelStateMachine); + // rightBumper.onFalse( + // lowerFlyWheel + // .setVelocity(RPM.of(0)) + // .andThen(lowerFlyWheel.set(0)) + // .alongWith(feeder.setSpeed(0))); } @Override public void setupDefaultCommands(Controller driver, Controller operator) { lowerFlyWheel.setDefaultCommand(lowerFlyWheel.joyStickControl(() -> driver.getRightTrigger())); upperFlyWheel.setDefaultCommand(upperFlyWheel.joyStickControl(() -> driver.getLeftTrigger())); + drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); } diff --git a/vendordeps/yams.json b/vendordeps/yams.json index 3339b3fb..1ce21095 100644 --- a/vendordeps/yams.json +++ b/vendordeps/yams.json @@ -1,7 +1,7 @@ { "fileName": "yams.json", "name": "Yet Another Mechanism System", - "version": "2025.11.10", + "version": "2025.11.20", "frcYear": "2025", "uuid": "a1051e86-a979-4880-a28b-a0d5362d1d96", "mavenUrls": [ @@ -12,7 +12,7 @@ { "groupId": "yams", "artifactId": "YAMS-java", - "version": "2025.11.10" + "version": "2025.11.20" } ], "cppDependencies": [], From 85619b0eac2a4edd08a9260245cfbbdde0857a4c Mon Sep 17 00:00:00 2001 From: Curt Date: Mon, 30 Mar 2026 18:20:56 -0400 Subject: [PATCH 49/93] Update swerve CAN IDs, zero offsets; adjust robots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reassign NEO CAN IDs and reset absolute encoder offsets for all four swerve modules to match new wiring: frontleft (drive 1→4, angle 2→3; angle inverted true→false), frontright (drive 7→2, angle 8→1), backleft (drive 3→6, angle 4→5), backright (drive 5→8, angle 6→7). Set absoluteEncoderOffset to 0 for each module (previous offsets removed). Also update robots.json: set the main robot "competition" flag to false and remove the basic_robot and baby_swerve entries to simplify deployment configs. --- .../yagsl_swerve/modules/backleft.json | 6 +++--- .../yagsl_swerve/modules/backright.json | 6 +++--- .../yagsl_swerve/modules/frontleft.json | 8 ++++---- .../yagsl_swerve/modules/frontright.json | 6 +++--- src/main/deploy/robots.json | 12 +----------- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json index 504e4b29..187c0fa9 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backleft.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 3, + "id": 6, "canbus": null }, "angle": { "type": "neo", - "id": 4, + "id": 5, "canbus": null }, "encoder": { @@ -19,7 +19,7 @@ "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 253.916, + "absoluteEncoderOffset": 0, "location": { "front": -9.2813, "left": 9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json index b58197cc..7356bcc7 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/backright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 5, + "id": 8, "canbus": null }, "angle": { "type": "neo", - "id": 6, + "id": 7, "canbus": null }, "encoder": { @@ -19,7 +19,7 @@ "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 237.832, + "absoluteEncoderOffset": 0, "location": { "front": -9.2813, "left": -9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json index 073845b7..7d857f81 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontleft.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 1, + "id": 4, "canbus": null }, "angle": { "type": "neo", - "id": 2, + "id": 3, "canbus": null }, "encoder": { @@ -16,10 +16,10 @@ }, "inverted": { "drive": false, - "angle": true + "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 117.773, + "absoluteEncoderOffset": 0, "location": { "front": 9.2813, "left": 9.2813 diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json index 30d60b2c..571e7b32 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/frontright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 7, + "id": 2, "canbus": null }, "angle": { "type": "neo", - "id": 8, + "id": 1, "canbus": null }, "encoder": { @@ -19,7 +19,7 @@ "angle": false }, "absoluteEncoderInverted": true, - "absoluteEncoderOffset": 333.896, + "absoluteEncoderOffset": 0, "location": { "front": 9.2813, "left": -9.2813 diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 31ca09a9..ad1396ac 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -5,17 +5,7 @@ "id": "00:80:2F:33:04:33", "robotClass": "frc.robot.blackteam.BlackRobot", "simulate": true, - "competition": true - }, - "basic_robot": { - "id": "basic_robot", - "robotClass": "frc.robot.example.ExampleRobot", - "simulate": false - }, - "baby_swerve": { - "id": "baby_swerve", - "robotClass": "frc.robot.baby_swerve.BabySwerve", - "simulate": false + "competition": false }, "buttercup_swerve": { "id": "00:80:2F:34:B2:C5", From 3e1a319cc580bdfc8f8b49a73a4fe7a474f6ed6a Mon Sep 17 00:00:00 2001 From: Curt Date: Wed, 1 Apr 2026 13:00:36 -0400 Subject: [PATCH 50/93] Add Javadoc workflow, docs, and schemas Add automated Javadoc generation and documentation plus wide-ranging project updates: create .github/workflows/javadoc.yml and docs/JAVADOC_GUIDELINES.md to generate and publish Javadocs; add Javadoc configuration and Lombok plugin in build.gradle and include archival entries in the jar task. Add many JSON schema files under src/main/resources/schemas and other resource changes (schemas, pictures, python helper script) to support validation and tooling. Update project metadata (.wpilib prefs, WPILib license year), adjust .gitignore, and update vendordeps. Large codebase changes include adding numerous new Java classes (units, drive/swerve configs, cameras, example subsystems, simulation helpers), moving/mirroring LobbinLoco assets into resources, and removing obsolete docs and the old swervelib package. These changes enable automated docs, schema-driven validation, and modernize the project for the 2026 toolchain. --- .github/workflows/javadoc.yml | 31 + .gitignore | 13 +- .wpilib/wpilib_preferences.json | 4 +- WPILib-License.md | 2 +- build.gradle | 28 +- docs/JAVADOC_GUIDELINES.md | 364 +++++ docs/JSON.md | 36 - docs/JSON_SCHEMAS.md | 354 ++++ docs/SCHEMA_VALIDATION_REPORT.md | 181 ++ docs/START.md | 47 - docs/UNIT_ENUM_DESIGN_PATTERN.md | 476 ++++++ docs/assets/css/responsive-borders.css | 187 --- docs/assets/css/v2.css | 15 - docs/assets/js/v2.js | 170 -- docs/index.html | 1269 -------------- docs/json/controllerproperties.md | 17 - docs/json/devicejson.md | 13 - docs/json/physicalproperties.md | 25 - docs/json/pidfjson.md | 27 - docs/json/pidfproperties.md | 13 - docs/json/swervedrive.md | 20 - docs/json/swervemodule.md | 27 - gradle/wrapper/gradle-wrapper.properties | 2 +- networktables.json.bck | 442 ----- pictures/AT23.png | Bin 0 -> 5630 bytes pictures/AT24.png | Bin 0 -> 5263 bytes pictures/AT25.png | Bin 0 -> 5971 bytes pictures/AT26.png | Bin 0 -> 5292 bytes pictures/AT27.png | Bin 0 -> 5411 bytes pictures/AT28.png | Bin 0 -> 5549 bytes pictures/AT29.png | Bin 0 -> 5446 bytes pictures/AT30.png | Bin 0 -> 5450 bytes pictures/AT31.png | Bin 0 -> 5829 bytes pictures/AT32.png | Bin 0 -> 5560 bytes pictures/{gpb.png => FRC5010Ball.png} | Bin pictures/gpa.png | Bin 2234 -> 43866 bytes pictures/robot.png | Bin 429616 -> 364856 bytes settings.gradle | 6 +- simgui-ds.json | 103 ++ src/main/deploy/baby_swerve/cameras.json | 4 +- .../basic_robot/akit_swerve_drivetrain.json | 177 +- src/main/deploy/basic_robot/cameras.json | 15 +- src/main/deploy/basic_robot/cameras/left.json | 12 + .../basic_robot/cameras/localization.json | 1 - .../deploy/basic_robot/cameras/right.json | 12 + .../deploy/basic_robot/field/game_pieces.json | 2 +- src/main/deploy/basic_robot/robot.json | 7 +- .../basic_robot/subsystems/example.json | 28 +- .../subsystems/example/percent_motor.json | 4 +- .../subsystems/example/yams_arm.json | 58 +- .../subsystems/example/yams_elevator.json | 43 +- .../subsystems/example/yams_pivot.json | 65 +- .../subsystems/example/yams_shooter.json | 42 +- src/main/java/frc/robot/BuildConstants.java | 10 +- src/main/java/frc/robot/Robot.java | 11 +- src/main/java/frc/robot/RobotContainer.java | 24 +- .../frc/robot/blackteam/FeederSubsystem.java | 2 +- .../java/frc/robot/example/ExampleRobot.java | 55 +- .../frc/robot/example/ExampleSubsystem.java | 249 --- .../example/commands/ExampleCommands.java | 131 ++ .../robot/example/subsystems/ExampleIO.java | 79 + .../example/subsystems/ExampleIOReal.java | 117 ++ .../example/subsystems/ExampleIOSim.java | 115 ++ .../example/subsystems/ExampleSubsystem.java | 179 ++ .../frc/robot/generated/TunerConstants.java | 320 ---- .../frc5010/common/arch/GenericMechanism.java | 22 +- .../org/frc5010/common/arch/GenericRobot.java | 36 +- .../org/frc5010/common/arch/StateMachine.java | 6 +- .../auto/pathplanner/PathFinderCommand.java | 4 +- .../common/commands/AkitDriveCommands.java | 551 +++++++ .../common/commands/DefaultDriveCommand.java | 1 - .../frc5010/common/commands/DriveByAngle.java | 1 - .../common/commands/DriveToPoseSupplier.java | 4 +- .../common/commands/LedDefaultCommand.java | 2 +- .../common/config/ConfigConstants.java | 7 + .../common/config/DeviceConfiguration.java | 38 + .../frc5010/common/config/RobotParser.java | 78 +- .../common/config/SubsystemParser.java | 1 + .../frc5010/common/config/UnitsParser.java | 461 ++---- .../config/json/AKitSwerveDrivetrainJson.java | 204 ++- .../config/json/CameraConfigurationJson.java | 145 +- .../common/config/json/GamePiecesJson.java | 4 +- .../config/json/VisionPropertiesJson.java | 6 +- .../config/json/YAGSLDrivetrainJson.java | 2 +- .../common/config/json/devices/ArmParser.java | 3 - .../json/devices/DeviceConfigReader.java | 66 +- .../json/devices/DrivetrainConstantsJson.java | 77 + .../GyroSettingsConfigurationJson.java | 11 +- .../config/json/devices/MotorSetupJson.java | 7 +- .../json/devices/MotorSystemIdJson.java | 16 +- .../PercentMotorConfigurationJson.java | 4 +- .../json/devices/ReflectionsManager.java | 18 +- .../config/json/devices/SubsystemJson.java | 16 +- .../VelocityMotorConfigurationJson.java | 3 +- .../devices/YamsArmConfigurationJson.java | 85 +- .../config/json/devices/YamsConfigCommon.java | 159 ++ .../YamsElevatorConfigurationJson.java | 75 +- .../devices/YamsPivotConfigurationJson.java | 83 +- .../devices/YamsShooterConfigurationJson.java | 124 +- .../common/config/units/AngleUnit.java | 43 + .../config/units/AngularAccelerationUnit.java | 54 + .../config/units/AngularVelocityUnit.java | 55 + .../common/config/units/CurrentUnit.java | 43 + .../common/config/units/DistanceUnit.java | 46 + .../config/units/LinearAccelerationUnit.java | 108 ++ .../config/units/LinearVelocityUnit.java | 58 + .../frc5010/common/config/units/MassUnit.java | 47 + .../config/units/MomentOfInertiaUnit.java | 41 + .../frc5010/common/config/units/TimeUnit.java | 47 + .../common/config/units/VoltageUnit.java | 44 + .../frc5010/common/constants/Constants.java | 2 + .../common/drive/DifferentialDrivetrain.java | 2 +- .../common/drive/DrivetrainConfig.java | 487 ++++++ .../common/drive/GenericDrivetrain.java | 57 +- .../common/drive/SimulatedDrivetrain.java | 3 +- .../common/drive/SwerveDriveConfig.java | 248 +++ .../common/drive/pose/DrivePoseEstimator.java | 2 - .../common/drive/pose/PoseProvider.java | 2 +- ...ervePose.java => SwerveFunctionsPose.java} | 11 +- .../common/drive/pose/YAGSLSwervePose.java | 57 - .../common/drive/swerve/AkitSwerveConfig.java | 239 +++ .../drive/swerve/GenericSwerveDrivetrain.java | 63 +- .../drive/swerve/SwerveDriveFunctions.java | 46 + .../drive/swerve/YAGSLSwerveDrivetrain.java | 64 +- .../drive/swerve/akit/AkitSwerveDrive.java | 201 ++- .../common/drive/swerve/akit/GyroIONavX.java | 4 +- .../drive/swerve/akit/GyroIOPigeon2.java | 26 +- .../common/drive/swerve/akit/GyroIOSim.java | 2 +- .../common/drive/swerve/akit/Module.java | 76 +- .../common/drive/swerve/akit/ModuleIO.java | 4 +- .../common/drive/swerve/akit/ModuleIOSim.java | 112 ++ .../drive/swerve/akit/ModuleIOSpark.java | 54 +- .../drive/swerve/akit/ModuleIOSparkTalon.java | 23 +- .../drive/swerve/akit/ModuleIOTalonFX.java | 49 +- .../swerve/akit/ModuleIOTalonFXReal.java | 11 +- .../drive/swerve/akit/ModuleIOTalonFXSim.java | 8 +- .../drive/swerve/akit/OdometryThread.java | 80 + .../swerve/akit/PhoenixOdometryThread.java | 42 +- .../swerve/akit/SparkOdometryThread.java | 60 +- .../swerve/akit/TalonFXOdometryThread.java | 145 ++ .../drive/swerve/akit/util/PhoenixUtil.java | 34 +- .../swerve_utils/PathConstraints5010.java | 25 +- .../frc5010/common/motors/MotorConstants.java | 15 + .../frc5010/common/motors/MotorFactory.java | 26 +- .../common/motors/SystemIdentification.java | 255 ++- .../motors/control/RevSparkController.java | 10 +- .../motors/control/ThriftyNovaController.java | 4 +- .../motors/function/PercentControlMotor.java | 4 + .../hardware/GenericRevBrushlessMotor.java | 42 +- .../motors/hardware/GenericTalonFXMotor.java | 17 +- .../motors/hardware/GenericTalonFXSMotor.java | 530 ++++++ .../hardware/GenericThriftyNovaMotor.java | 8 +- .../frc5010/common/sensors/ThriftyLimit.java | 2 +- .../common/sensors/camera/GenericCamera.java | 20 + .../PhotonVisionFiducialTargetCamera.java | 4 +- .../camera/PhotonVisionPoseCamera.java | 51 +- .../sensors/camera/QuestNavInterface.java | 331 ++++ .../{QuestNav.java => QuestNavOld.java} | 6 +- .../sensors/camera/SimulatedCamera.java | 5 +- .../camera/SimulatedFiducialTargetCamera.java | 4 +- .../camera/SimulatedVisualTargetCamera.java | 4 +- .../sensors/encoder/CanCoderEncoder.java | 3 +- .../common/sensors/encoder/RevEncoder.java | 6 +- .../sensors/encoder/ThriftyNovaEncoder.java | 10 +- .../common/subsystems/CameraSystem.java | 107 +- .../frc5010/common/subsystems/LEDStrip.java | 4 +- .../common/telemetry/DisplayAngle.java | 2 +- .../common/telemetry/DisplayCurrent.java | 21 +- .../common/telemetry/DisplayDouble.java | 2 +- .../common/telemetry/DisplayFloat.java | 2 +- .../common/telemetry/DisplayLength.java | 30 +- .../frc5010/common/telemetry/DisplayLong.java | 2 +- .../common/telemetry/DisplayString.java | 2 +- .../frc5010/common/telemetry/DisplayTime.java | 12 +- .../common/telemetry/DisplayVoltage.java | 47 +- .../utils/geometry/AllianceFlipUtil.java | 62 + .../common/utils/geometry/GeomUtil.java | 174 ++ .../org/frc5010/common/vision/AprilTags.java | 69 +- .../common/vision/VisionConstants.java | 4 +- .../lobbinloco/FRC5010BallOnField.java | 2 +- .../lobbinloco/FRC5010BallOnTheFly.java | 2 +- .../org/frc5010/lobbinloco/LobbinLoco.java | 4 +- src/main/java/org/frc5010/package-info.java | 2 +- src/main/java/swervelib/SwerveController.java | 223 --- src/main/java/swervelib/SwerveDrive.java | 1456 ----------------- src/main/java/swervelib/SwerveDriveTest.java | 477 ------ .../java/swervelib/SwerveInputStream.java | 914 ----------- src/main/java/swervelib/SwerveModule.java | 890 ---------- .../encoders/AnalogAbsoluteEncoderSwerve.java | 126 -- .../swervelib/encoders/CANCoderSwerve.java | 211 --- .../swervelib/encoders/CanAndMagSwerve.java | 97 -- .../encoders/DIODutyCycleEncoderSwerve.java | 106 -- .../encoders/SparkFlexEncoderSwerve.java | 136 -- .../encoders/SparkMaxAnalogEncoderSwerve.java | 185 --- .../encoders/SparkMaxEncoderSwerve.java | 190 --- .../encoders/SwerveAbsoluteEncoder.java | 64 - .../encoders/TalonSRXEncoderSwerve.java | 82 - .../encoders/ThriftyNovaEncoderSwerve.java | 110 -- .../java/swervelib/encoders/package-info.java | 5 - .../java/swervelib/imu/ADIS16448Swerve.java | 120 -- .../java/swervelib/imu/ADIS16470Swerve.java | 121 -- .../java/swervelib/imu/ADXRS450Swerve.java | 115 -- .../java/swervelib/imu/AnalogGyroSwerve.java | 120 -- .../java/swervelib/imu/CanandgyroSwerve.java | 117 -- src/main/java/swervelib/imu/NavXSwerve.java | 137 -- .../java/swervelib/imu/Pigeon2Swerve.java | 151 -- src/main/java/swervelib/imu/PigeonSwerve.java | 121 -- .../imu/PigeonViaTalonSRXSwerve.java | 130 -- src/main/java/swervelib/imu/SwerveIMU.java | 69 - src/main/java/swervelib/imu/package-info.java | 2 - src/main/java/swervelib/math/Matter.java | 32 - src/main/java/swervelib/math/SwerveMath.java | 394 ----- .../java/swervelib/math/package-info.java | 6 - .../swervelib/motors/SparkFlexSwerve.java | 455 ------ .../motors/SparkMaxBrushedMotorSwerve.java | 555 ------- .../java/swervelib/motors/SparkMaxSwerve.java | 456 ------ .../java/swervelib/motors/SwerveMotor.java | 195 --- .../java/swervelib/motors/TalonFXSSwerve.java | 399 ----- .../java/swervelib/motors/TalonFXSwerve.java | 402 ----- .../java/swervelib/motors/TalonSRXSwerve.java | 440 ----- .../swervelib/motors/ThriftyNovaSwerve.java | 461 ------ .../java/swervelib/motors/package-info.java | 2 - src/main/java/swervelib/package-info.java | 6 - src/main/java/swervelib/parser/Cache.java | 87 - .../java/swervelib/parser/PIDFConfig.java | 88 - .../parser/SwerveControllerConfiguration.java | 57 - .../parser/SwerveDriveConfiguration.java | 139 -- .../parser/SwerveModuleConfiguration.java | 141 -- .../SwerveModulePhysicalCharacteristics.java | 112 -- .../java/swervelib/parser/SwerveParser.java | 180 -- .../parser/deserializer/PIDFRange.java | 10 - .../deserializer/ReflectionsManager.java | 82 - .../parser/deserializer/package-info.java | 2 - .../parser/json/ControllerPropertiesJson.java | 30 - .../swervelib/parser/json/DeviceJson.java | 301 ---- .../swervelib/parser/json/ModuleJson.java | 122 -- .../parser/json/MotorConfigDouble.java | 24 - .../swervelib/parser/json/MotorConfigInt.java | 24 - .../parser/json/PIDFPropertiesJson.java | 12 - .../parser/json/PhysicalPropertiesJson.java | 75 - .../parser/json/SwerveDriveJson.java | 15 - .../modules/AngleConversionFactorsJson.java | 27 - .../parser/json/modules/BoolMotorJson.java | 10 - .../json/modules/ConversionFactorsJson.java | 40 - .../modules/DriveConversionFactorsJson.java | 29 - .../parser/json/modules/LocationJson.java | 14 - .../parser/json/modules/package-info.java | 2 - .../swervelib/parser/json/package-info.java | 2 - .../java/swervelib/parser/package-info.java | 2 - .../simulation/SwerveIMUSimulation.java | 98 -- .../simulation/SwerveModuleSimulation.java | 91 -- .../swervelib/simulation/package-info.java | 2 - .../telemetry/SwerveDriveTelemetry.java | 346 ---- .../swervelib/telemetry/package-info.java | 2 - .../calculateRobotToCameraTransform.py | 80 + .../lobbinloco/LobbinLoco.json | 0 .../lobbinloco/PathPlannerCustomField.png | Bin .../lobbinloco/image.png | Bin .../userAssets/Field2d_LobbinLoco/config.json | 0 .../userAssets/Field2d_LobbinLoco/image.png | Bin .../Field3d_LobbinLoco/LobbinLoco.glb | Bin .../userAssets/Field3d_LobbinLoco/config.json | 0 .../userAssets/Field3d_LobbinLoco/model.glb | Bin .../userAssets/Robot_Cart/config.json | 0 .../userAssets/Robot_Cart/model.glb | Bin .../userAssets/Robot_LobbinLoco/config.json | 0 .../userAssets/Robot_LobbinLoco/model.glb | Bin .../akit_swerve_drivetrain.schema.json | 418 +++++ .../schemas/angle-unit-value.schema.json | 29 + ...ngular-acceleration-unit-value.schema.json | 48 + .../angular-velocity-unit-value.schema.json | 42 + src/main/resources/schemas/camera.schema.json | 87 + .../resources/schemas/cameras.schema.json | 20 + .../resources/schemas/controllers.schema.json | 20 + .../schemas/current-unit-value.schema.json | 37 + .../schemas/distance-unit-value.schema.json | 40 + .../driveteam-controller-axis.schema.json | 56 + .../schemas/driveteam-controller.schema.json | 32 + .../resources/schemas/led-strip.schema.json | 64 + ...linear-acceleration-unit-value.schema.json | 85 + .../linear-velocity-unit-value.schema.json | 53 + .../schemas/mass-unit-value.schema.json | 45 + .../resources/schemas/motor-setup.schema.json | 99 ++ .../schemas/motor-system-id.schema.json | 80 + .../schemas/percent-motor.schema.json | 75 + src/main/resources/schemas/robot.schema.json | 112 ++ src/main/resources/schemas/robots.schema.json | 69 + .../schemas/time-unit-value.schema.json | 44 + .../resources/schemas/unit-value.schema.json | 20 + .../resources/schemas/user-mode.schema.json | 32 + .../schemas/velocity-motor.schema.json | 111 ++ .../schemas/voltage-unit-value.schema.json | 35 + .../resources/schemas/yams-arm.schema.json | 64 + .../schemas/yams-elevator.schema.json | 65 + .../resources/schemas/yams-pivot.schema.json | 62 + .../schemas/yams-shooter.schema.json | 54 + vendordeps/AdvantageKit.json | 8 +- ...rLib.json => PathplannerLib-2026.1.2.json} | 10 +- ...atest.json => Phoenix5-replay-5.36.0.json} | 112 +- ...on => Phoenix6-replay-frc2026-latest.json} | 156 +- vendordeps/REVLib.json | 76 +- ...b-2025.0.1.json => ReduxLib-2026.1.1.json} | 25 +- .../{Studica-2025.0.1.json => Studica.json} | 94 +- ...riftyLib.json => ThriftyLib-2026.1.0.json} | 10 +- vendordeps/WPILibNewCommands.json | 3 +- ...plefrc2025.json => libgrapplefrc2026.json} | 19 +- vendordeps/maple-sim.json | 26 - vendordeps/photonlib.json | 14 +- vendordeps/questnavlib.json | 21 + vendordeps/yagsl-2026.2.27.1.json | 46 + vendordeps/yams.json | 9 +- 311 files changed, 10957 insertions(+), 16880 deletions(-) create mode 100644 .github/workflows/javadoc.yml create mode 100644 docs/JAVADOC_GUIDELINES.md delete mode 100644 docs/JSON.md create mode 100644 docs/JSON_SCHEMAS.md create mode 100644 docs/SCHEMA_VALIDATION_REPORT.md delete mode 100644 docs/START.md create mode 100644 docs/UNIT_ENUM_DESIGN_PATTERN.md delete mode 100644 docs/assets/css/responsive-borders.css delete mode 100644 docs/assets/css/v2.css delete mode 100644 docs/assets/js/v2.js delete mode 100644 docs/index.html delete mode 100644 docs/json/controllerproperties.md delete mode 100644 docs/json/devicejson.md delete mode 100644 docs/json/physicalproperties.md delete mode 100644 docs/json/pidfjson.md delete mode 100644 docs/json/pidfproperties.md delete mode 100644 docs/json/swervedrive.md delete mode 100644 docs/json/swervemodule.md delete mode 100644 networktables.json.bck create mode 100644 pictures/AT23.png create mode 100644 pictures/AT24.png create mode 100644 pictures/AT25.png create mode 100644 pictures/AT26.png create mode 100644 pictures/AT27.png create mode 100644 pictures/AT28.png create mode 100644 pictures/AT29.png create mode 100644 pictures/AT30.png create mode 100644 pictures/AT31.png create mode 100644 pictures/AT32.png rename pictures/{gpb.png => FRC5010Ball.png} (100%) create mode 100644 simgui-ds.json create mode 100644 src/main/deploy/basic_robot/cameras/left.json create mode 100644 src/main/deploy/basic_robot/cameras/right.json delete mode 100644 src/main/java/frc/robot/example/ExampleSubsystem.java create mode 100644 src/main/java/frc/robot/example/commands/ExampleCommands.java create mode 100644 src/main/java/frc/robot/example/subsystems/ExampleIO.java create mode 100644 src/main/java/frc/robot/example/subsystems/ExampleIOReal.java create mode 100644 src/main/java/frc/robot/example/subsystems/ExampleIOSim.java create mode 100644 src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java delete mode 100644 src/main/java/frc/robot/generated/TunerConstants.java create mode 100644 src/main/java/org/frc5010/common/commands/AkitDriveCommands.java create mode 100644 src/main/java/org/frc5010/common/config/json/devices/DrivetrainConstantsJson.java create mode 100644 src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java create mode 100644 src/main/java/org/frc5010/common/config/units/AngleUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/AngularAccelerationUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/AngularVelocityUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/CurrentUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/DistanceUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/LinearAccelerationUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/LinearVelocityUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/MassUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/MomentOfInertiaUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/TimeUnit.java create mode 100644 src/main/java/org/frc5010/common/config/units/VoltageUnit.java create mode 100644 src/main/java/org/frc5010/common/drive/DrivetrainConfig.java create mode 100644 src/main/java/org/frc5010/common/drive/SwerveDriveConfig.java rename src/main/java/org/frc5010/common/drive/pose/{AkitSwervePose.java => SwerveFunctionsPose.java} (80%) delete mode 100644 src/main/java/org/frc5010/common/drive/pose/YAGSLSwervePose.java create mode 100644 src/main/java/org/frc5010/common/drive/swerve/AkitSwerveConfig.java create mode 100644 src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSim.java create mode 100644 src/main/java/org/frc5010/common/drive/swerve/akit/OdometryThread.java create mode 100644 src/main/java/org/frc5010/common/drive/swerve/akit/TalonFXOdometryThread.java create mode 100644 src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXSMotor.java create mode 100644 src/main/java/org/frc5010/common/sensors/camera/QuestNavInterface.java rename src/main/java/org/frc5010/common/sensors/camera/{QuestNav.java => QuestNavOld.java} (98%) create mode 100644 src/main/java/org/frc5010/common/utils/geometry/AllianceFlipUtil.java create mode 100644 src/main/java/org/frc5010/common/utils/geometry/GeomUtil.java delete mode 100644 src/main/java/swervelib/SwerveController.java delete mode 100644 src/main/java/swervelib/SwerveDrive.java delete mode 100644 src/main/java/swervelib/SwerveDriveTest.java delete mode 100644 src/main/java/swervelib/SwerveInputStream.java delete mode 100644 src/main/java/swervelib/SwerveModule.java delete mode 100644 src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/CANCoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/CanAndMagSwerve.java delete mode 100644 src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java delete mode 100644 src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java delete mode 100644 src/main/java/swervelib/encoders/package-info.java delete mode 100644 src/main/java/swervelib/imu/ADIS16448Swerve.java delete mode 100644 src/main/java/swervelib/imu/ADIS16470Swerve.java delete mode 100644 src/main/java/swervelib/imu/ADXRS450Swerve.java delete mode 100644 src/main/java/swervelib/imu/AnalogGyroSwerve.java delete mode 100644 src/main/java/swervelib/imu/CanandgyroSwerve.java delete mode 100644 src/main/java/swervelib/imu/NavXSwerve.java delete mode 100644 src/main/java/swervelib/imu/Pigeon2Swerve.java delete mode 100644 src/main/java/swervelib/imu/PigeonSwerve.java delete mode 100644 src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java delete mode 100644 src/main/java/swervelib/imu/SwerveIMU.java delete mode 100644 src/main/java/swervelib/imu/package-info.java delete mode 100644 src/main/java/swervelib/math/Matter.java delete mode 100644 src/main/java/swervelib/math/SwerveMath.java delete mode 100644 src/main/java/swervelib/math/package-info.java delete mode 100644 src/main/java/swervelib/motors/SparkFlexSwerve.java delete mode 100644 src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java delete mode 100644 src/main/java/swervelib/motors/SparkMaxSwerve.java delete mode 100644 src/main/java/swervelib/motors/SwerveMotor.java delete mode 100644 src/main/java/swervelib/motors/TalonFXSSwerve.java delete mode 100644 src/main/java/swervelib/motors/TalonFXSwerve.java delete mode 100644 src/main/java/swervelib/motors/TalonSRXSwerve.java delete mode 100644 src/main/java/swervelib/motors/ThriftyNovaSwerve.java delete mode 100644 src/main/java/swervelib/motors/package-info.java delete mode 100644 src/main/java/swervelib/package-info.java delete mode 100644 src/main/java/swervelib/parser/Cache.java delete mode 100644 src/main/java/swervelib/parser/PIDFConfig.java delete mode 100644 src/main/java/swervelib/parser/SwerveControllerConfiguration.java delete mode 100644 src/main/java/swervelib/parser/SwerveDriveConfiguration.java delete mode 100644 src/main/java/swervelib/parser/SwerveModuleConfiguration.java delete mode 100644 src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java delete mode 100644 src/main/java/swervelib/parser/SwerveParser.java delete mode 100644 src/main/java/swervelib/parser/deserializer/PIDFRange.java delete mode 100644 src/main/java/swervelib/parser/deserializer/ReflectionsManager.java delete mode 100644 src/main/java/swervelib/parser/deserializer/package-info.java delete mode 100644 src/main/java/swervelib/parser/json/ControllerPropertiesJson.java delete mode 100644 src/main/java/swervelib/parser/json/DeviceJson.java delete mode 100644 src/main/java/swervelib/parser/json/ModuleJson.java delete mode 100644 src/main/java/swervelib/parser/json/MotorConfigDouble.java delete mode 100644 src/main/java/swervelib/parser/json/MotorConfigInt.java delete mode 100644 src/main/java/swervelib/parser/json/PIDFPropertiesJson.java delete mode 100644 src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java delete mode 100644 src/main/java/swervelib/parser/json/SwerveDriveJson.java delete mode 100644 src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java delete mode 100644 src/main/java/swervelib/parser/json/modules/BoolMotorJson.java delete mode 100644 src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java delete mode 100644 src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java delete mode 100644 src/main/java/swervelib/parser/json/modules/LocationJson.java delete mode 100644 src/main/java/swervelib/parser/json/modules/package-info.java delete mode 100644 src/main/java/swervelib/parser/json/package-info.java delete mode 100644 src/main/java/swervelib/parser/package-info.java delete mode 100644 src/main/java/swervelib/simulation/SwerveIMUSimulation.java delete mode 100644 src/main/java/swervelib/simulation/SwerveModuleSimulation.java delete mode 100644 src/main/java/swervelib/simulation/package-info.java delete mode 100644 src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java delete mode 100644 src/main/java/swervelib/telemetry/package-info.java create mode 100644 src/main/resources/calculateRobotToCameraTransform.py rename src/main/{java/org/frc5010 => resources}/lobbinloco/LobbinLoco.json (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/PathPlannerCustomField.png (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/image.png (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Field2d_LobbinLoco/config.json (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Field2d_LobbinLoco/image.png (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Field3d_LobbinLoco/config.json (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Field3d_LobbinLoco/model.glb (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Robot_Cart/config.json (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Robot_Cart/model.glb (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Robot_LobbinLoco/config.json (100%) rename src/main/{java/org/frc5010 => resources}/lobbinloco/userAssets/Robot_LobbinLoco/model.glb (100%) create mode 100644 src/main/resources/schemas/akit_swerve_drivetrain.schema.json create mode 100644 src/main/resources/schemas/angle-unit-value.schema.json create mode 100644 src/main/resources/schemas/angular-acceleration-unit-value.schema.json create mode 100644 src/main/resources/schemas/angular-velocity-unit-value.schema.json create mode 100644 src/main/resources/schemas/camera.schema.json create mode 100644 src/main/resources/schemas/cameras.schema.json create mode 100644 src/main/resources/schemas/controllers.schema.json create mode 100644 src/main/resources/schemas/current-unit-value.schema.json create mode 100644 src/main/resources/schemas/distance-unit-value.schema.json create mode 100644 src/main/resources/schemas/driveteam-controller-axis.schema.json create mode 100644 src/main/resources/schemas/driveteam-controller.schema.json create mode 100644 src/main/resources/schemas/led-strip.schema.json create mode 100644 src/main/resources/schemas/linear-acceleration-unit-value.schema.json create mode 100644 src/main/resources/schemas/linear-velocity-unit-value.schema.json create mode 100644 src/main/resources/schemas/mass-unit-value.schema.json create mode 100644 src/main/resources/schemas/motor-setup.schema.json create mode 100644 src/main/resources/schemas/motor-system-id.schema.json create mode 100644 src/main/resources/schemas/percent-motor.schema.json create mode 100644 src/main/resources/schemas/robot.schema.json create mode 100644 src/main/resources/schemas/robots.schema.json create mode 100644 src/main/resources/schemas/time-unit-value.schema.json create mode 100644 src/main/resources/schemas/unit-value.schema.json create mode 100644 src/main/resources/schemas/user-mode.schema.json create mode 100644 src/main/resources/schemas/velocity-motor.schema.json create mode 100644 src/main/resources/schemas/voltage-unit-value.schema.json create mode 100644 src/main/resources/schemas/yams-arm.schema.json create mode 100644 src/main/resources/schemas/yams-elevator.schema.json create mode 100644 src/main/resources/schemas/yams-pivot.schema.json create mode 100644 src/main/resources/schemas/yams-shooter.schema.json rename vendordeps/{PathplannerLib.json => PathplannerLib-2026.1.2.json} (84%) rename vendordeps/{Phoenix5-frc2025-latest.json => Phoenix5-replay-5.36.0.json} (63%) rename vendordeps/{Phoenix6-frc2025-latest.json => Phoenix6-replay-frc2026-latest.json} (83%) rename vendordeps/{ReduxLib-2025.0.1.json => ReduxLib-2026.1.1.json} (74%) rename vendordeps/{Studica-2025.0.1.json => Studica.json} (70%) rename vendordeps/{ThriftyLib.json => ThriftyLib-2026.1.0.json} (62%) rename vendordeps/{libgrapplefrc2025.json => libgrapplefrc2026.json} (83%) delete mode 100644 vendordeps/maple-sim.json create mode 100644 vendordeps/questnavlib.json create mode 100644 vendordeps/yagsl-2026.2.27.1.json diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml new file mode 100644 index 00000000..038051f3 --- /dev/null +++ b/.github/workflows/javadoc.yml @@ -0,0 +1,31 @@ +name: Generate Javadocs + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Build project + run: ./gradlew build + + - name: Generate Javadocs + run: ./gradlew javadoc + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: build/docs/javadoc \ No newline at end of file diff --git a/.gitignore b/.gitignore index d271052c..34cbaac1 100644 --- a/.gitignore +++ b/.gitignore @@ -106,7 +106,6 @@ Temporary Items ### VisualStudioCode ### .vscode/* !.vscode/settings.json -!.vscode/settings.prefs !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json @@ -169,16 +168,20 @@ out/ # Fleet .fleet +# Simulation GUI and other tools window save file +networktables.json +simgui.json +*-window.json + # Simulation data log directory logs/ -simgui-window.json -simgui-ds.json - # Folder that has CTRE Phoenix Sim device config storage ctre_sim/ # clangd /.cache compile_commands.json -networktables.json.bck + +# Eclipse generated file for annotation processors +.factorypath diff --git a/.wpilib/wpilib_preferences.json b/.wpilib/wpilib_preferences.json index 6681556e..423137dd 100644 --- a/.wpilib/wpilib_preferences.json +++ b/.wpilib/wpilib_preferences.json @@ -1,6 +1,6 @@ { - "currentLanguage": "java", "enableCppIntellisense": false, - "projectYear": "none", + "currentLanguage": "java", + "projectYear": "2026", "teamNumber": 5010 } diff --git a/WPILib-License.md b/WPILib-License.md index 645e5425..eb3061b0 100644 --- a/WPILib-License.md +++ b/WPILib-License.md @@ -1,4 +1,4 @@ -Copyright (c) 2009-2024 FIRST and other WPILib contributors +Copyright (c) 2009-2026 FIRST and other WPILib contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/build.gradle b/build.gradle index 52578893..d555a2bf 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,26 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2025.3.2" + id "edu.wpi.first.GradleRIO" version "2026.2.1" id "com.peterabeles.gversion" version "1.10" id "com.diffplug.spotless" version "6.12.0" + id "io.freefair.lombok" version "8.4" +} + +javadoc { + dependsOn compileJava + mustRunAfter compileJava + inputs.files sourceSets.main.output + source = fileTree('src/main/java') + fileTree('build/generated/sources/annotationProcessor/java/main') + classpath = sourceSets.main.compileClasspath + options { + encoding = 'UTF-8' + charSet = 'UTF-8' + author = true + version = true + links('https://docs.oracle.com/en/java/javase/17/docs/api/') + windowTitle = 'FRC5010Lib Javadocs' + docTitle = 'FRC5010Lib Javadocs' + } } java { @@ -50,14 +68,14 @@ deploy { // longer exist in deploy directory of this project } } - } } } def deployArtifact = deploy.targets.roborio.artifacts.frcJava -// Set to true to use debug for JNI. +// Set to true to use debug for all targets including JNI, which will drastically impact +// performance. wpi.java.debugJni = false // Set this to true to enable desktop support. @@ -120,7 +138,9 @@ wpi.sim.addDriverstation() // knows where to look for our Robot Class. jar { from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } - from sourceSets.main.allSource + from('src') { into 'backup/src' } + from('vendordeps') { into 'backup/vendordeps' } + from('build.gradle') { into 'backup' } manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) duplicatesStrategy = DuplicatesStrategy.INCLUDE } diff --git a/docs/JAVADOC_GUIDELINES.md b/docs/JAVADOC_GUIDELINES.md new file mode 100644 index 00000000..05758024 --- /dev/null +++ b/docs/JAVADOC_GUIDELINES.md @@ -0,0 +1,364 @@ +# Javadoc Guidelines for FRC5010Example + +This document provides guidelines for writing effective Javadoc comments throughout the FRC5010Example codebase. + +## Table of Contents + +- [Overview](#overview) +- [Javadoc Standards](#javadoc-standards) +- [File Categories](#file-categories) +- [Examples](#examples) +- [Automated Documentation Generation](#automated-documentation-generation) + +## Overview + +Javadoc is an essential tool for documenting Java code. It improves code maintainability, helps new team members understand the codebase, and generates professional API documentation. This project uses Gradle to automatically generate HTML documentation from Javadoc comments. + +### Key Benefits + +- **API Documentation**: Auto-generates professional HTML documentation +- **IDE Integration**: Javadoc comments appear in IDE tooltips and autocomplete +- **Code Clarity**: Forces developers to think about the purpose and usage of their code +- **Maintenance**: Future developers can quickly understand complex implementations + +## Javadoc Standards + +### Class Documentation Template + +Every public class should have a class-level Javadoc comment that includes: + +```java +/** + * Brief one-line description of the class. + * + *

More detailed explanation of what this class does, how it's used, + * and any important constraints or requirements. This can span multiple + * paragraphs and include examples if helpful. + * + *

Usage example: + *

+ *   MyClass instance = new MyClass();
+ *   instance.doSomething();
+ * 
+ * + * @see RelatedClass + * @see RelatedInterface + */ +public class MyClass { + // implementation +} +``` + +### Field Documentation + +Public fields (especially configuration data) should be documented: + +```java +/** + * Brief description of what this field represents. + * + *

Include details about: + *

    + *
  • The unit of measurement (if applicable)
  • + *
  • Valid value ranges
  • + *
  • Default values
  • + *
  • Any special behavior or side effects
  • + *
+ */ +public double configValue = 0.0; +``` + +### Method Documentation + +Every public method should include: + +```java +/** + * Brief one-line description of what this method does. + * + *

More detailed description of the method's behavior, including: + *

    + *
  • What side effects it may have
  • + *
  • What happens in edge cases
  • + *
  • How it interacts with other methods/systems
  • + *
+ * + *

Example usage: + *

+ *   result = myMethod(value1, value2);
+ * 
+ * + * @param param1 description of the first parameter, including type info + * @param param2 description of the second parameter + * @return description of the return value and its meaning + * @throws IOException if an I/O error occurs + * @throws IllegalArgumentException if param1 is negative + * @see #relatedMethod() + */ +public ReturnType myMethod(Type param1, Type param2) throws IOException { + // implementation +} +``` + +### Constructor Documentation + +Constructors should document initialization: + +```java +/** + * Creates a new instance with the specified configuration. + * + *

This constructor initializes all required components and validates + * the input parameters. + * + * @param name the unique name identifier for this instance + * @param config the configuration object (must not be null) + * @throws NullPointerException if config is null + * @throws IllegalArgumentException if name is empty + */ +public MyClass(String name, Configuration config) { + // implementation +} +``` + +## File Categories + +### 1. Configuration Classes (`src/main/java/org/frc5010/common/config/json/`) + +Configuration classes load JSON data and configure robot subsystems. These should document: +- What JSON structure they expect +- How the configuration affects robot behavior +- Any constraints on configuration values + +**Current status**: Improved in progress +- ✅ `CameraConfigurationJson` - Enhanced with comprehensive field and method documentation + +**Files needing Javadoc**: +- `DriveteamControllerConfiguration.java` +- `RobotJson.java` +- `VisionPropertiesJson.java` +- `DrivetrainPropertiesJson.java` +- `*ConfigurationJson.java` device configuration classes + +**Example** (already implemented): +```java +/** + * Configuration data class for camera systems in an FRC robot. + * + *

This class represents the JSON configuration for a single camera, including its type + * (Limelight, PhotonVision, AprilTag), physical pose relative to the robot center, and + * calibration parameters. + */ +public class CameraConfigurationJson { +``` + +### 2. Parser Classes (`src/main/java/org/frc5010/common/config/`) + +Parser classes read and process configuration files. They should document: +- What files they read +- The structure of expected data +- Error conditions and exceptions + +**Current status**: Improved in progress +- ✅ `RobotParser` - Enhanced with detailed documentation about the configuration loading process + +**Files needing Javadoc**: +- `SubsystemParser.java` +- `RobotsParser.java` +- `UnitsParser.java` + +### 3. Subsystem Classes (`src/main/java/org/frc5010/common/subsystems/`) + +Subsystem classes implement robot functionality. Document: +- The purpose and behavior of the subsystem +- Key methods and their effects +- Important timing or state considerations + +**Files needing Javadoc**: +- `CameraSystem.java` +- `AprilTagPoseSystem.java` +- `LEDStrip.java` +- `PowerDistribution5010.java` + +### 4. Vision/Sensor Classes (`src/main/java/org/frc5010/common/sensors/`) + +Vision and sensor classes interact with hardware. Document: +- What hardware they interface with +- Return value meanings and units +- Calibration and setup requirements + +**Files needing Javadoc**: +- All camera implementations +- Sensor implementations (gyro, limit switches, etc.) + +### 5. Motor/Drive Classes (`src/main/java/org/frc5010/common/motors/`) + +Motor control classes need clear documentation about: +- Control modes and their parameters +- Units and scale factors +- Hardware limitations + +**Files needing Javadoc**: +- `GenericMotorController.java` and implementations +- `AngularControlMotor.java` +- `VelocityControlMotor.java` +- `PositionControlMotor.java` + +### 6. Utility Classes (`src/main/java/org/frc5010/common/`) + +Utility classes provide helper functions. Document: +- What transformations they perform +- Input/output formats +- Use cases + +**Files needing Javadoc**: +- `AllianceFlip.java` +- `RobotIdentity.java` +- `SystemIdentification.java` + +## Examples + +### Example 1: Configuration Class + +```java +/** + * JSON configuration for Limelight camera systems. + * + *

This class deserializes Limelight-specific configuration from JSON + * and provides methods to instantiate and configure Limelight cameras + * on the robot. + * + *

Typical JSON structure: + *

+ * {
+ *   "name": "limelight",
+ *   "type": "limelight",
+ *   "use": "apriltag",
+ *   "x": 0.0,
+ *   "y": 0.0,
+ *   "z": 0.5
+ * }
+ * 
+ */ +public class LimeLightConfigurationJson implements CameraConfiguration { +``` + +### Example 2: Complex Method + +```java +/** + * Computes the robot's pose using multiple vision targets with outlier rejection. + * + *

This method processes all tracked AprilTag detections and uses a + * weighted average to estimate the robot's position and orientation. + * Low-confidence detections are automatically filtered out using the + * ambiguity threshold. + * + *

The algorithm: + *

    + *
  1. Filters detections by confidence and ambiguity ratio
  2. + *
  3. Computes individual pose estimates from each tag
  4. + *
  5. Weights estimates by detection confidence
  6. + *
  7. Returns the weighted average pose
  8. + *
+ * + * @param detections the list of AprilTag detections from the camera + * @param cameraToRobot the transform from camera coordinates to robot center + * @return the estimated robot pose in field coordinates, or empty if no valid estimates + * @throws NullPointerException if detections or cameraToRobot is null + * @see AprilTagDetection + */ +public Optional estimatePose(List detections, + Transform3d cameraToRobot) { +``` + +### Example 3: Enum or Constants Class + +```java +/** + * Standard AprilTag field layout and utilities for pose estimation. + * + *

This class provides access to the official FIRST AprilTag field layout + * for the current game season. It also includes utility methods for + * common AprilTag-related calculations. + * + *

Usage: + *

+ *   AprilTagFieldLayout layout = AprilTags.aprilTagFieldLayout;
+ *   Optional tagPose = layout.getTagPose(tagID);
+ * 
+ */ +public class AprilTags { + /** The AprilTag field layout for the 2026 game season */ + public static AprilTagFieldLayout aprilTagFieldLayout; +``` + +## Automated Documentation Generation + +### Building Javadocs Locally + +To generate Javadoc HTML documentation on your machine: + +```bash +./gradlew javadoc +``` + +The generated HTML will be in `build/docs/javadoc/`. + +### Viewing Javadocs + +1. **In your IDE**: Hover over any class, method, or field to see Javadoc tooltips +2. **In a browser**: Open `build/docs/javadoc/index.html` after running the Gradle task +3. **GitHub Pages**: The `javadoc.yml` workflow automatically deploys docs on push to `main` + +### GitHub Pages Deployment + +The project includes a GitHub Actions workflow (`.github/workflows/javadoc.yml`) that: +1. Checks out the code +2. Builds the project +3. Generates Javadocs +4. Publishes them to GitHub Pages + +Your documentation will be automatically deployed when you push to the `main` branch. + +## Implementation Priority + +To ensure the most critical parts of the codebase are well-documented, prioritize adding Javadoc in this order: + +1. **High Priority** - Core public APIs + - `CameraConfigurationJson` ✅ + - `RobotParser` ✅ + - `DeviceConfiguration` ✅ + - All public interfaces + +2. **Medium Priority** - Configuration classes + - `RobotJson` + - `DriveteamControllerConfiguration` + - All JSON configuration classes + +3. **Medium Priority** - Major subsystems + - `GenericRobot` + - `GenericDrivetrain` + - `CameraSystem` and implementations + +4. **Lower Priority** - Utilities and helpers + - Utility functions + - Helper classes + - Test utilities + +## Maintenance Notes + +- When adding new public classes or methods, always include Javadoc before committing +- Use `@deprecated` tags when removing old APIs +- Keep Javadoc synchronized with code changes +- Review Javadoc warnings during builds + +## Tools and Resources + +- [Official Javadoc Guide](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html) +- [Javadoc Tag Reference](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#tags) +- [Best Practices for Javadoc](https://developer.oracle.com/java/javadoc-best-practices) + +## Questions? + +For questions about Javadoc standards in this project, refer to this document or check existing examples in the improved files. diff --git a/docs/JSON.md b/docs/JSON.md deleted file mode 100644 index c7dbeba7..00000000 --- a/docs/JSON.md +++ /dev/null @@ -1,36 +0,0 @@ -# JSON Files - -JSON stands for "JavaScript Object Notation" it is a popular format for configuration, and string -data representation. Learn more [here](https://www.w3schools.com/js/js_json_intro.asp) - -# What does the swerve directory need to look like? - -The swerve directory for generating a swerve drive must look like this. Assuming you have defined -your swerve modules -as `"modules": [ "frontleft.json", "frontright.json", "backleft.json", "backright.json"]` -in [`swervedrive.json`](../src/main/deploy/swerve/swervedrive.json). - -```text -└── swerve - ├── controllerproperties.json - ├── modules - │   ├── backleft.json - │   ├── backright.json - │   ├── frontleft.json - │   ├── frontright.json - │   ├── physicalproperties.json - │   └── pidfproperties.json - └── swervedrive.json -``` - -The `modules` folder, `controllerproperties.json`, `physicalproperties.json`, `pidfproperties.json`, -and `swervedrive.json` files are necessary to build the swerve drive. Each one has specific fields -which correspond to an attribute of the swerve drive, some physical and some abstract. - -### Click on each JSON below to see the configuration options - -* [`swervedrive.json`](json/swervedrive.md) -* [`controllerproperties.json`](json/controllerproperties.md) -* [`pidfpropreties.json`](json/pidfproperties.md) -* [`physicalproperties.json`](json/physicalproperties.md) -* [`backleft.json`, `backright.json`, `frontleft.json`, `frontright.json`](json/swervemodule.md) diff --git a/docs/JSON_SCHEMAS.md b/docs/JSON_SCHEMAS.md new file mode 100644 index 00000000..d108fda3 --- /dev/null +++ b/docs/JSON_SCHEMAS.md @@ -0,0 +1,354 @@ +# JSON Schema Configuration Summary + +This document describes all JSON schemas configured in VSCode for the FRC5010 project. + +## Schemas Overview + +All JSON schemas are stored in: `src/main/java/org/frc5010/common/config/schemas/` + +### 1. **robot.json** → `robot.schema.json` +**Location:** `src/main/deploy/*/robot.json` + +**Java Class:** `RobotJson.java` + +**Purpose:** Main robot configuration file containing drivetrain settings, game piece definitions, and simulation options. + +**Key Fields:** +- `userConfig` - Reference to user mode file (e.g., competition_mode.json) +- `driveType` - Drivetrain type (YAGSL_SWERVE_DRIVE, AKIT_SWERVE_DRIVE) +- `trackWidth`, `wheelBase`, `wheelDiameter` - Drivetrain dimensions with units +- `physicalMaxSpeed` - Maximum robot speed capability +- `driveMotorGearRatio` - Gear ratio between motor and wheels +- `loadSimulatedField` - Whether to load simulated field +- `gamePieceA`, `gamePieceB` - Game piece definitions + +--- + +### 2. **competition_mode.json** / **demo_mode.json** → `user-mode.schema.json` +**Location:** `src/main/deploy/*/competition_mode.json`, `src/main/deploy/*/demo_mode.json` + +**Java Class:** `UserModeJson.java` + +**Purpose:** Defines operational limits for different robot modes (competition, demo, practice, etc.) + +**Key Fields:** +- `maxSpeed` - Maximum teleop speed (m/s) +- `maxAngularSpeed` - Maximum angular velocity (rad/s) +- `maxAccelleration` - Maximum linear acceleration (m/s²) +- `maxAngularAccelleration` - Maximum angular acceleration (rad/s²) + +--- + +### 3. **cameras.json** → `cameras.schema.json` +**Location:** `src/main/deploy/*/cameras.json` + +**Java Class:** `CameraConfigurationJson.java` + +**Purpose:** List of camera configuration files to load + +**Key Fields:** +- `cameras` - Array of camera configuration filenames in the cameras/ directory + +--- + +### 4. **cameras/*.json** → `camera.schema.json` +**Location:** `src/main/deploy/*/cameras/*.json` (e.g., intake.json, localization.json) + +**Java Class:** `CameraConfigurationJson.java` + +**Purpose:** Configuration for individual camera systems (PhotonVision, Limelight, etc.) + +**Key Fields:** +- `name` - Unique camera identifier +- `use` - Use case (target, apriltag, quest) +- `type` - Camera type (limelight, photonvision) +- `strategy` - Pose estimation strategy +- `x`, `y`, `z` - Position offsets from robot center (meters) +- `roll`, `pitch`, `yaw` - Rotation angles (degrees) +- `width`, `height` - Camera resolution (pixels) + +--- + +### 5. **controllers.json** → `controllers.schema.json` +**Location:** `src/main/deploy/*/controllers.json` + +**Java Class:** `DriveteamControllersJson.java` + +**Purpose:** List of driveteam controller configuration files + +**Key Fields:** +- `controllers` - Array of controller configuration filenames in the controllers/ directory + +--- + +### 6. **controllers/*.json** → `driveteam-controller.schema.json` +**Location:** `src/main/deploy/*/controllers/*.json` (e.g., driver.json, operator.json) + +**Java Class:** `DriveteamControllerJson.java` + +**Purpose:** Configuration for individual driveteam controllers (driver, operator, etc.) + +**Key Fields:** +- `name` - Controller name (driver, operator, etc.) +- `port` - USB port number (0-5) +- `axis` - Array of axis configuration filenames in controllers/axis/ directory + +--- + +### 7. **controllers/axis/*.json** → `driveteam-controller-axis.schema.json` +**Location:** `src/main/deploy/*/controllers/axis/*.json` + +**Java Class:** `DriveteamControllerAxisJson.java` + +**Purpose:** Configuration for individual joystick/controller axes + +**Key Fields:** +- `channel` - Axis channel number (0-11) +- `deadband` - Minimum threshold to register motion (0.0-1.0) +- `invert` - Whether to invert axis +- `scale` - Output scaling factor +- `curvePower` - Curve power for non-linear response (1.0 = linear) +- `limit` - Maximum output limit (0.0-1.0) +- `rate` - Slew rate limiting + +--- + +### 8. **robots.json** → `robots.schema.json` +**Location:** `src/main/deploy/robots.json` + +**Java Class:** `RobotsJson.java` + +**Purpose:** Top-level configuration mapping robot identities to robot classes + +**Key Fields:** +- `competitionPin` - DIO pin for competition selector (-1 to disable) +- `robots` - Object mapping robot IDs to configurations + - `id` - MAC address or unique identifier + - `robotClass` - Fully qualified Java class name + - `simulate` - Whether to use in simulation + - `competition` - Whether to use when competition pin is active + +--- + +### 9. **akit_swerve_drivetrain.json** → `akit_swerve_drivetrain.schema.json` +**Location:** `src/main/deploy/*/akit_swerve_drivetrain.json` + +**Java Class:** `AKitSwerveDrivetrainJson.java` + +**Purpose:** Detailed AKit swerve drivetrain configuration with motor control and module setup + +**Key Fields:** +- `type` - Motor controller type (TalonFX, SparkTalon, Spark) +- `constants` - Physical and control constants + - Dimensions and masses + - Motor control parameters (PID, feedforward) + - Module configurations (drive motor, steer motor, encoder setup) + - CAN bus configuration + +--- + +## Subsystem Configuration Schemas + +### 10. **led_strip.json** → `led-strip.schema.json` +**Location:** `src/main/deploy/*/led_strip.json` + +**Java Class:** `LEDStripConfigJson.java` + +**Purpose:** Configuration for addressable LED strips (WS2812, APA102, etc.) + +**Key Fields:** +- `length` - Total number of LEDs +- `dataPin` - PWM pin number for LED control +- `segments` - Array of named LED segments (e.g., status_indicator, team_colors) + +--- + +### 11. **percent_motor.json** → `percent-motor.schema.json` +**Location:** `src/main/deploy/*/subsystems/example/percent_motor.json` + +**Java Class:** `PercentMotorConfigurationJson.java` + +**Purpose:** Configuration for a simple percent-output motor (no closed-loop control) + +**Key Fields:** +- `name` - Subsystem name +- `controller` - Controller type (spark, talonfx, talonsrx, sparkmax) +- `type` - Motor type (Neo, KrakenX60, Falcon500) +- `id` - CAN ID +- `gearing` - Motor gearing ratio +- `momentOfInertiaKgMSq` - Moment of inertia for simulation +- `x`, `y`, `z` - Position for visualization (meters) +- `logLevel` - Telemetry logging level + +--- + +### 12. **velocity_motor.json** → `velocity-motor.schema.json` +**Location:** `src/main/deploy/*/subsystems/example/velocity_motor.json` + +**Java Class:** `VelocityMotorConfigurationJson.java` + +**Purpose:** Configuration for a velocity-controlled motor with PID and feedforward + +**Key Fields:** +- `name` - Subsystem name +- `controller` - Controller type +- `type` - Motor type +- `id` - CAN ID +- `gearing` - Motor gearing ratio +- `momentOfInertiaKgMSq` - Moment of inertia for simulation +- `x`, `y`, `z` - Position for visualization (meters) +- `kP`, `kI`, `kD` - PID gains +- `kS`, `kV`, `kA` - Feedforward constants +- `iZone` - PID integral zone +- `logLevel` - Telemetry logging level + +--- + +### 13. **yams_arm.json** → `yams-arm.schema.json` +**Location:** `src/main/deploy/*/subsystems/example/yams_arm.json` + +**Java Class:** `YamsArmConfigurationJson.java` + +**Purpose:** Configuration for a YAMS robotic arm with angular positioning (joints, shoulders, etc.) + +**Key Fields:** +- `motorSetup` - Motor controller configuration +- `motorSystemId` - PID and feedforward parameters +- `length` - Arm effective length +- `lowerHardLimit`, `upperHardLimit` - Hardware angle limits +- `lowerSoftLimit`, `upperSoftLimit` - Software angle limits +- `startingAngle` - Initial angle at startup +- `gearing` - Gear reduction stages +- `mass` - Arm mass +- `horizontalZero` - Angle at which arm is horizontal + +--- + +### 14. **yams_elevator.json** → `yams-elevator.schema.json` +**Location:** `src/main/deploy/*/subsystems/example/yams_elevator.json` + +**Java Class:** `YamsElevatorConfigurationJson.java` + +**Purpose:** Configuration for a YAMS elevator mechanism with linear positioning + +**Key Fields:** +- `motorSetup` - Motor controller configuration +- `motorSystemId` - PID and feedforward parameters +- `sprocketTeeth` - Number of sprocket teeth (for chain drive) +- `drumRadius` - Drum radius (for direct drive) +- `lowerHardLimit`, `upperHardLimit` - Hardware height limits (meters) +- `lowerSoftLimit`, `upperSoftLimit` - Software height limits (meters) +- `gearing` - Gear reduction stages +- `startingPosition` - Initial height at startup +- `mass` - Carriage and payload mass + +--- + +### 15. **yams_pivot.json** → `yams-pivot.schema.json` +**Location:** `src/main/deploy/*/subsystems/example/yams_pivot.json` + +**Java Class:** `YamsPivotConfigurationJson.java` + +**Purpose:** Configuration for a YAMS pivot mechanism (turret rotation, wrist, etc.) + +**Key Fields:** +- `motorSetup` - Motor controller configuration +- `motorSystemId` - PID and feedforward parameters +- `lowerHardLimit`, `upperHardLimit` - Hardware angle limits +- `lowerSoftLimit`, `upperSoftLimit` - Software angle limits +- `startingAngle` - Initial angle at startup +- `gearing` - Gear reduction stages +- `moi` - Moment of inertia + +--- + +### 16. **yams_shooter.json** → `yams-shooter.schema.json` +**Location:** `src/main/deploy/*/subsystems/example/yams_shooter.json` + +**Java Class:** `YamsShooterConfigurationJson.java` + +**Purpose:** Configuration for a YAMS flywheel shooter mechanism + +**Key Fields:** +- `motorSetup` - Motor controller configuration +- `motorSystemId` - PID and feedforward parameters +- `lowerSoftLimit`, `upperSoftLimit` - Velocity limits (RPM or deg/s) +- `gearing` - Gear reduction stages +- `mass` - Flywheel mass +- `diameter` - Flywheel diameter +- `moi` - Moment of inertia + +--- + +## Shared Utility Schemas + +These schemas are referenced by multiple configuration files: + +### **unit-value.schema.json** +Represents a numeric value with unit of measure. Used for all physical measurements. + +```json +{ + "val": 123.45, + "uom": "meters" +} +``` + +### **motor-setup.schema.json` +Motor controller hardware configuration. Referenced by YAMS subsystem schemas. + +**Fields:** +- `name` - Motor name +- `controllerType` - Controller type +- `motorType` - Motor model +- `canId` - CAN identifier +- `canBus` - CAN bus name (optional) +- `idleMode` - BRAKE or COAST +- `currentLimit` - Current limit with units +- `inverted` - Motor inversion flag +- `followers` - Array of follower motor configs + +### **motor-system-id.schema.json** +PID and feedforward control parameters. Referenced by YAMS subsystem and velocity motor schemas. + +**Fields:** +- `closedLoopRamp` - Ramp rate +- `openLoopRamp` - Ramp rate +- `feedBack` - PID constants (p, i, d) +- `feedForward` - FF constants (s, g, v, a) +- `maxVelocity` - Velocity limit +- `maxAcceleration` - Acceleration limit +- `controlMode` - CLOSED_LOOP or OPEN_LOOP + +--- + +## VSCode Configuration + +All schemas are configured in `.vscode/settings.json` under the `json.schemas` array. Each schema entry specifies: +- `fileMatch` - Glob pattern(s) to match against file paths +- `url` - Relative path to the schema file + +## Usage + +Once configured: +1. Open any JSON file matching one of the patterns above +2. VSCode will automatically provide: + - **Intellisense/autocomplete** for available properties + - **Validation** against the schema + - **Hover documentation** for each field + - **Error highlighting** for invalid values + +## Adding New Schemas + +To add a new schema: +1. Create the schema file in `src/main/java/org/frc5010/common/config/schemas/` +2. Add an entry to `.vscode/settings.json` under `json.schemas` +3. Reload VSCode window for changes to take effect + +## Schema Testing + +To test if schemas are working: +1. Open a JSON file matching one of the patterns +2. Check the VSCode status bar (bottom right) +3. You should see schema information or validation errors +4. Hover over properties to see documentation diff --git a/docs/SCHEMA_VALIDATION_REPORT.md b/docs/SCHEMA_VALIDATION_REPORT.md new file mode 100644 index 00000000..a590db32 --- /dev/null +++ b/docs/SCHEMA_VALIDATION_REPORT.md @@ -0,0 +1,181 @@ +# Schema Validation Report +## akit_swerve_drivetrain.json vs AKitSwerveDrivetrainJson.java + +This report validates that the JSON schema correctly enforces the structure required by the `AKitSwerveDrivetrainJson.java` class and its dependency `DrivetrainConstantsJson.java`. + +--- + +## Top-Level Properties + +### AKitSwerveDrivetrainJson Class Fields +| Field | Type | Java Default | Schema Enforcement | +|-------|------|-------------|--------------------| +| `type` | String | "SparkTalon" | ✅ enum: ["TalonFX", "SparkTalon", "Spark"] | +| `constants` | DrivetrainConstantsJson | Required | ✅ Required object | +| `gamePiecesJson` | Optional | Empty | ⚠️ Not in JSON (internal field) | + +--- + +## DrivetrainConstantsJson Properties + +### Required Fields +All these fields are required in the schema and Java class: + +| Field | Java Type | Schema Type | Validation | +|-------|-----------|-------------|-----------| +| `trackWidth` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `wheelBase` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `wheelDiameter` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `bumperFrameWidth` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `bumperFrameLength` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `maxDriveSpeed` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `gyro` | GyroSettingsConfigurationJson | object | ✅ See gyro details below | +| `driveGearRatio` | String | string | ✅ Format: "1:ratio" | +| `steerGearRatio` | String | string | ✅ Format: "1:ratio" | +| `driveMotorControl` | MotorSystemIdJson | motorControl | ✅ See motor control details below | +| `steerMotorControl` | MotorSystemIdJson | motorControl | ✅ See motor control details below | +| `modules` | Map | object | ✅ Properties: frontLeft, frontRight, backLeft, backRight | +| `coupleRatio` | double | number | ✅ Numeric value | +| `invertLeftSide` | boolean | boolean | ✅ True/false | +| `invertRightSide` | boolean | boolean | ✅ True/false | +| `steerInertia` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `driveInertia` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `robotMass` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `wheelCOF` | double | number | ✅ Numeric value (coefficient of friction) | +| `slipCurrent` | UnitValueJson | measurement | ✅ {val: number, uom: string} | +| `canbus` | String | string | ✅ CAN bus name | + +--- + +## Gyroscope Configuration + +### GyroSettingsConfigurationJson Structure +| Field | Java Type | Schema Validation | +|-------|-----------|------------------| +| `type` | String | ✅ enum: ["pigeon2", "pigeon1", "navx", "adxrs450", "yagsl"] | +| `id` | int | ✅ integer (0-62) | +| `inverted` | boolean | ✅ boolean | +| `canbus` | String | ✅ string | + +**Java Implementation Note:** The type field is case-sensitive. Valid values from the code are: +- `"pigeon2"` - Phoenix Pigeon 2 gyro +- `"pigeon1"` - Phoenix Pigeon 1 gyro +- `"navx"` - NavX gyroscope +- `"yagsl"` - YAGSL gyroscope (from YAGSL library) + +--- + +## Motor System ID Configuration + +### MotorSystemIdJson Structure +| Field | Java Type | Schema Validation | Notes | +|-------|-----------|------------------|-------| +| `closedLoopRamp` | UnitValueJson | ✅ measurement | Default: 0.25 seconds | +| `openLoopRamp` | UnitValueJson | ✅ measurement | Default: 0.25 seconds | +| `feedBack` | FeedBack object | ✅ object | Contains p, i, d | +| `feedBack.p` | double | ✅ number | Proportional gain | +| `feedBack.i` | double | ✅ number | Integral gain | +| `feedBack.d` | double | ✅ number | Derivative gain | +| `maxVelocity` | UnitValueJson | ✅ measurement | Default: 0 | +| `maxAcceleration` | UnitValueJson | ✅ measurement | Default: 0 | +| `feedForward` | FeedForward object | ✅ object | Contains s, g, v, a | +| `feedForward.s` | double | ✅ number | Static constant | +| `feedForward.g` | double | ✅ number | Gravity constant | +| `feedForward.v` | double | ✅ number | Velocity constant | +| `feedForward.a` | double | ✅ number | Acceleration constant | +| `controlMode` | String | ✅ enum | "CLOSED_LOOP" or "OPEN_LOOP" | + +**⚠️ Important Note:** The current akit_swerve_drivetrain.json has simplified feedforward (only s, v, a), but the schema now validates the complete MotorSystemIdJson structure which includes the gravity constant 'g'. + +--- + +## Motor Setup Configuration + +### MotorSetupJson Structure +| Field | Java Type | Schema Validation | Notes | +|-------|-----------|------------------|-------| +| `name` | String | ✅ Required string | Descriptive name | +| `logLevel` | String | ✅ enum | "LOW", "MEDIUM", "HIGH" | +| `motorType` | String | ✅ string | e.g., "KrakenX60", "Falcon500", "NEO" | +| `controllerType` | String | ✅ enum | "talonfx", "talonsrx", "sparkmax", "victorspx" | +| `canId` | int | ✅ Required int (0-62) | CAN identifier | +| `canBus` | String | ✅ string | Default: "" (rio bus) | +| `idleMode` | String | ✅ enum | "BRAKE" or "COAST" | +| `currentLimit` | UnitValueJson | ✅ measurement | Default: 40 amps | +| `inverted` | boolean | ✅ boolean | Motor inversion | +| `numberOfMotors` | int | ✅ int (min: 1) | Includes followers | +| `followers` | FollowerMotorJson[] | ✅ array | Follower configuration | +| `robotToMotor` | Translation3dJson | ✅ object | Motor position relative to robot | +| `movementPlane` | String | ✅ enum | "XY", "XZ", "YZ" | + +**Schema Validation for Followers:** +```json +{ + "canId": integer (0-62), + "inverted": boolean +} +``` + +--- + +## Swerve Module Configuration + +### ModuleConfigJson Structure (4 instances: frontLeft, frontRight, backLeft, backRight) +| Field | Java Type | Schema Validation | +|-------|-----------|------------------| +| `driveMotorSetup` | MotorSetupJson | ✅ motorSetup object | +| `steerMotorSetup` | MotorSetupJson | ✅ motorSetup object | +| `encoderId` | int | ✅ integer (0-15) | +| `absoluteOffset` | UnitValueJson | ✅ measurement (rotations or degrees) | +| `encoderInverted` | boolean | ✅ boolean | + +--- + +## Schema Compliance Summary + +✅ **Fully Compliant:** +- Type enum values match Java code +- All required fields are enforced +- Nested object structures match Java classes +- CAN ID ranges (0-62) are validated +- Motor movement planes validated +- Idle mode enum enforced +- Control mode enum enforced + +⚠️ **Partially Compliant (Loose):** +- `motorType` - Schema allows any string (Java code may have specific types) +- `movementPlane` rotations - Currently allows any string, could be more restrictive +- `uom` (units of measure) - Allows any string, could define valid units explicitly + +🔄 **Motor Control Differences:** +- Original JSON uses simplified structure (s, v, a only) +- Java class `MotorSystemIdJson` has full structure (s, g, v, a) +- Schema enforces complete structure - **May require JSON file updates** + +--- + +## Recommendations + +1. **Update motor control definitions** in JSON files to include all fields from `MotorSystemIdJson`: + - Add `closedLoopRamp`, `openLoopRamp`, `maxVelocity`, `maxAcceleration`, `controlMode` + - Add `g` (gravity) to feedForward constants + +2. **Consider stricter motor type validation** - Define enum of supported motor types + +3. **Define valid UOM (units of measure) values** - Create enum for common units like: + - inches, feet, meters, mm + - rad, deg, rotations + - m/sec, m/sec^2, rad/sec + - kg, lbs, kg*m^2 + +4. **Test the schema against current JSON file** to identify any incompatibilities + +--- + +## Related Java Classes +- `AKitSwerveDrivetrainJson.java` - Main configuration class +- `DrivetrainConstantsJson.java` - Constants holder +- `MotorSystemIdJson.java` - Motor control parameters +- `MotorSetupJson.java` - Individual motor setup +- `GyroSettingsConfigurationJson.java` - Gyroscope configuration +- `UnitValueJson.java` - Unit value representation diff --git a/docs/START.md b/docs/START.md deleted file mode 100644 index 1fae7084..00000000 --- a/docs/START.md +++ /dev/null @@ -1,47 +0,0 @@ -# How to create a swerve drive? - -YAGSL is unique in the fact that you can create a swerve drive based entirely off of JSON -configuration files. The JSON configuration files should be located in -the [`deploy`](../src/main/deploy) directory. You can also create the Configuration objects manually -and instantiate your Swerve Drive that way. - -## How to create a SwerveDrive using JSON. - -This example program creates -the [`SwerveDrive`](../src/main/java/frc/robot/subsystems/swervedrive/swervelib/SwerveDrive.java) -in the [`SwerveSubsystem`](../src/main/java/frc/robot/subsystems/swervedrive/SwerveSubsystem.java), -as you should only interact with it in the SwerveSubsystem if you are using command based -programming. - -```java -import java.io.File; -import edu.wpi.first.wpilibj.Filesystem; -import swervelib.parser.SwerveParser; -import swervelib.SwerveDrive; - -File swerveJsonDirectory=new File(Filesystem.getDeployDirectory(),"swerve"); - SwerveDrive swerveDrive=new SwerveParser(swerveJsonDirectory).createSwerveDrive(); -``` - -This way is fast and easy, no more large unmaintainable and daunting constants file to worry about! -To create a JSON directory look at the [JSON documentation](JSON.md). - -# Creating a swerve drive. - -- [ ] Install NavX, CTRE and REV vendor dependencies. -- [ ] Clone YAGSL. `git clone https://github.com/BroncBotz3481/YAGSL` -- [ ] Move [swervelib](https://github.com/BroncBotz3481/YAGSL/tree/main/swervelib) - into `src/main/frc/java/subsystems/swervedrive/swerve` -- [ ] Copy [example JSON directory](https://github.com/BroncBotz3481/YAGSL/tree/main/deploy) or - create your own then move it into `src/main/frc/deploy/swerve` -- [ ] Create SwerveDrive from JSON directory - via `SwerveDrive drive = new SwerveParser(new File(Filesystem.getDeployDirectory(), "swerve")).createSwerveDrive();` -- [ ] View the java docs in [docs/index.html](https://broncbotz3481.github.io/YAGSL/) -- [ ] Experiment! - -# Contributions - -This library is based off of -95's [SwervyBot code](https://github.com/first95/FRC2023/tree/main/SwervyBot) in addition to a wide -variety of other code bases. -A huge thank you to every team which has open sourced their swerve code! diff --git a/docs/UNIT_ENUM_DESIGN_PATTERN.md b/docs/UNIT_ENUM_DESIGN_PATTERN.md new file mode 100644 index 00000000..996ee460 --- /dev/null +++ b/docs/UNIT_ENUM_DESIGN_PATTERN.md @@ -0,0 +1,476 @@ +# Unit Enum Design Pattern + +This document explains the design pattern used for all unit enums in the FRC5010Example project. + +## Pattern Overview + +```java +public enum [UnitType]Unit { + ENUM_VALUE_1("alias1", "alias2", "alias3"), + ENUM_VALUE_2("alias1", "alias2"), + // ... more values + ; + + private final String[] aliases; + + [UnitType]Unit(String... aliases) { + this.aliases = aliases; + } + + public static [UnitType]Unit fromString(String unitString) { + // Error handling + // Normalization + // Alias matching + // Clear error message + } + + public String[] getAliases() { + return aliases; + } +} +``` + +## Complete Example: DistanceUnit + +```java +package org.frc5010.common.config.units; + +/** + * Enum representing distance unit types. + * Each enum value can be parsed from multiple string representations. + */ +public enum DistanceUnit { + METERS("m", "meter", "meters"), + INCHES("in", "inch", "inches"), + FEET("ft", "foot", "feet"), + MILLIMETERS("mm", "millimeter", "millimeters"), + CENTIMETERS("cm", "centimeter", "centimeters"), + YARDS("yd", "yard", "yards"); + + private final String[] aliases; + + DistanceUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a DistanceUnit. + * + * @param unitString The string representation of the unit + * @return The matching DistanceUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static DistanceUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (DistanceUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown distance unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} +``` + +## Design Decisions Explained + +### 1. Constructor with Varargs + +```java +DistanceUnit(String... aliases) { + this.aliases = aliases; +} +``` + +**Why?** +- Allows flexible alias definition (1 or many) +- Clean syntax for declaration +- Type-safe + +**Usage:** +```java +METERS("m", "meter", "meters") // 3 aliases +FEET("ft", "foot", "feet") // 3 aliases +INCHES("in", "inch", "inches") // 3 aliases +``` + +### 2. Static Factory Method + +```java +public static DistanceUnit fromString(String unitString) { + // ... +} +``` + +**Why?** +- Standard pattern for parsing strings to enums +- Provides clear intent +- Allows custom parsing logic +- Named method is clearer than casting + +**Usage:** +```java +DistanceUnit unit = DistanceUnit.fromString("meters"); +// vs +DistanceUnit unit = (DistanceUnit) Enum.valueOf(DistanceUnit.class, "METERS"); +``` + +### 3. Null Checking + +```java +if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); +} +``` + +**Why?** +- Fail fast with clear error message +- Prevent cryptic NullPointerException +- Consistent error handling + +**Result:** +``` +Exception: Unit string cannot be null +// vs +java.lang.NullPointerException + at DistanceUnit.fromString(DistanceUnit.java:45) +``` + +### 4. String Normalization + +```java +String normalized = unitString.trim().toLowerCase(); +``` + +**Why?** +- Handles whitespace (e.g., " meters ") +- Case-insensitive input (e.g., "Meters", "METERS") +- Improves user experience + +**Results:** +``` +DistanceUnit.fromString(" METERS ") ✓ Works +DistanceUnit.fromString("meters") ✓ Works +DistanceUnit.fromString("Meters") ✓ Works +``` + +### 5. Comprehensive Error Messages + +```java +throw new IllegalArgumentException("Unknown distance unit: " + unitString); +``` + +**Why?** +- Shows user what they provided +- Clear action needed (choose valid unit) +- Helps with debugging + +**Result:** +``` +Exception: Unknown distance unit: meteres +// User can see they misspelled "meters" +``` + +### 6. Getter for Introspection + +```java +public String[] getAliases() { + return aliases; +} +``` + +**Why?** +- Allows code to discover valid aliases +- Useful for documentation +- Enables dynamic validation + +**Usage:** +```java +for (String alias : DistanceUnit.METERS.getAliases()) { + System.out.println(alias); // m, meter, meters +} +``` + +## Enum Integration with UnitsParser + +Each enum is integrated into UnitsParser with a dedicated parsing method: + +```java +public static Distance parseDistance(double magnitude, String unit) { + DistanceUnit distanceUnit = DistanceUnit.fromString(unit); + switch (distanceUnit) { + case METERS: + return Meters.of(magnitude); + case INCHES: + return Inches.of(magnitude); + case FEET: + return Feet.of(magnitude); + case MILLIMETERS: + return Millimeters.of(magnitude); + case CENTIMETERS: + return Centimeters.of(magnitude); + case YARDS: + return Feet.of(magnitude * 3); + default: + System.err.println("Unexpected distance unit: " + distanceUnit); + return Meters.of(magnitude); + } +} +``` + +**Benefits:** +- Enum parsing separated from conversion +- Clear separation of concerns +- Each enum responsible for validation +- Each method responsible for conversion + +## JSON Schema Integration + +Each enum has a corresponding JSON schema that validates enum values: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Distance Unit Value", + "type": "object", + "required": ["val", "uom"], + "properties": { + "val": { + "type": "number", + "description": "The numeric distance value" + }, + "uom": { + "type": "string", + "enum": [ + "m", "meter", "meters", + "in", "inch", "inches", + "ft", "foot", "feet", + "mm", "millimeter", "millimeters", + "cm", "centimeter", "centimeters", + "yd", "yard", "yards" + ], + "description": "Distance unit (meters, inches, feet, mm, cm, or yards)" + } + } +} +``` + +**Relationship:** +``` +DistanceUnit enum values + ↓ + + aliases for each value + ↓ +JSON schema enum constraint + ↓ +VSCode validation feedback +``` + +## Extending the Pattern + +### Adding a New Unit Type + +1. **Create the enum class:** +```java +public enum FrequencyUnit { + HERTZ("hz", "hertz"), + KILOHERTZ("khz", "kilohertz"), + MEGAHERTZ("mhz", "megahertz"); + + private final String[] aliases; + + FrequencyUnit(String... aliases) { + this.aliases = aliases; + } + + public static FrequencyUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (FrequencyUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown frequency unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} +``` + +2. **Add parsing method to UnitsParser:** +```java +public static Frequency parseFrequency(double magnitude, String unit) { + FrequencyUnit freqUnit = FrequencyUnit.fromString(unit); + switch (freqUnit) { + case HERTZ: + return Hertz.of(magnitude); + case KILOHERTZ: + return Hertz.of(magnitude * 1000); + case MEGAHERTZ: + return Hertz.of(magnitude * 1000000); + default: + System.err.println("Unexpected frequency unit: " + freqUnit); + return Hertz.of(magnitude); + } +} +``` + +3. **Create JSON schema:** +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Frequency Unit Value", + "type": "object", + "required": ["val", "uom"], + "properties": { + "val": { + "type": "number" + }, + "uom": { + "type": "string", + "enum": ["hz", "hertz", "khz", "kilohertz", "mhz", "megahertz"] + } + } +} +``` + +## Pattern Characteristics + +| Aspect | Implementation | +|--------|---| +| **Safety** | Type-safe enum values, compile-time checked | +| **Flexibility** | Multiple aliases per unit, case-insensitive | +| **Error Handling** | Clear exceptions with actionable messages | +| **Extensibility** | Easy to add new units or aliases | +| **Performance** | O(n*m) parsing, negligible for typical use | +| **Thread Safety** | Enums are singletons, inherently thread-safe | +| **Testability** | Easy to unit test fromString() method | +| **Documentation** | Self-documenting enum values | +| **Integration** | Works seamlessly with JSON schemas | + +## Comparison with Alternatives + +### Before (String Constants) +```java +public static final String M = "m"; +public static final String CM = "cm"; +// ... 30+ more constants + +public static Distance parseDistance(double magnitude, String unit) { + switch (unit.trim().toLowerCase()) { + case M: + case "meter": + case "meters": + return Meters.of(magnitude); + // ... many more cases + } +} +``` + +**Cons:** +- No type safety +- Scattered string cases +- Error validation at runtime +- Hard to find all units + +### After (Enums) +```java +public enum DistanceUnit { + METERS("m", "meter", "meters"), + // ... more units +} + +public static Distance parseDistance(double magnitude, String unit) { + DistanceUnit distanceUnit = DistanceUnit.fromString(unit); + switch (distanceUnit) { + case METERS: + return Meters.of(magnitude); + // ... more cases + } +} +``` + +**Pros:** +- Type-safe +- Centralized definitions +- Error validation earlier +- Clear error messages +- Easy to extend + +## Best Practices + +1. **Always null check** - Provide clear error for null input +2. **Always normalize** - Handle whitespace and case variations +3. **Always validate** - Use enums to ensure valid values +4. **Always document** - Explain supported aliases +5. **Always test** - Unit test fromString() thoroughly +6. **Always extend** - Use the pattern for new unit types +7. **Always integrate** - Create corresponding JSON schemas + +## Testing the Pattern + +```java +@Test +public void testValidUnits() { + assertEquals(DistanceUnit.METERS, DistanceUnit.fromString("meters")); + assertEquals(DistanceUnit.FEET, DistanceUnit.fromString("ft")); + assertEquals(DistanceUnit.INCHES, DistanceUnit.fromString("in")); +} + +@Test +public void testCaseInsensitive() { + assertEquals(DistanceUnit.METERS, DistanceUnit.fromString("METERS")); + assertEquals(DistanceUnit.FEET, DistanceUnit.fromString("FT")); +} + +@Test +public void testWhitespace() { + assertEquals(DistanceUnit.METERS, DistanceUnit.fromString(" meters ")); +} + +@Test +public void testInvalidUnit() { + assertThrows(IllegalArgumentException.class, + () -> DistanceUnit.fromString("invalid")); +} + +@Test +public void testNullUnit() { + assertThrows(IllegalArgumentException.class, + () -> DistanceUnit.fromString(null)); +} + +@Test +public void testAliases() { + String[] aliases = DistanceUnit.METERS.getAliases(); + assertThat(aliases).contains("m", "meter", "meters"); +} +``` + +## Conclusion + +This enum pattern provides: +- **Type Safety** - Compile-time checking +- **Validation** - Early error detection +- **Clarity** - Self-documenting code +- **Maintainability** - Centralized definitions +- **Extensibility** - Easy to add new units +- **Integration** - Works with JSON schemas + +It's the recommended approach for any system that needs to parse and validate string-based configurations. diff --git a/docs/assets/css/responsive-borders.css b/docs/assets/css/responsive-borders.css deleted file mode 100644 index d02a8e05..00000000 --- a/docs/assets/css/responsive-borders.css +++ /dev/null @@ -1,187 +0,0 @@ -@media (min-width: 576px) { - .border-sm-top { - border-top: 1px solid #e3e7eb !important; - } - .border-sm-right { - border-right: 1px solid #e3e7eb !important; - } - .border-sm-bottom { - border-bottom: 1px solid #e3e7eb !important; - } - .border-sm-left { - border-left: 1px solid #e3e7eb !important; - } - .border-sm-top-0 { - border-top: 0 !important; - } - .border-sm-right-0 { - border-right: 0 !important; - } - .border-sm-bottom-0 { - border-bottom: 0 !important; - } - .border-sm-left-0 { - border-left: 0 !important; - } - .border-sm-x { - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-sm-y { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - } - .border-sm { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-sm-0 { - border-top: 0 !important; - border-bottom: 0 !important; - border-left: 0 !important; - border-right: 0 !important; - } - } - - @media (min-width: 768px) { - .border-md-top { - border-top: 1px solid #e3e7eb !important; - } - .border-md-right { - border-right: 1px solid #e3e7eb !important; - } - .border-md-bottom { - border-bottom: 1px solid #e3e7eb !important; - } - .border-md-left { - border-left: 1px solid #e3e7eb !important; - } - .border-md-top-0 { - border-top: 0 !important; - } - .border-md-right-0 { - border-right: 0 !important; - } - .border-md-bottom-0 { - border-bottom: 0 !important; - } - .border-md-left-0 { - border-left: 0 !important; - } - .border-md-x { - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-md-y { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - } - .border-md { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-md-0 { - border-top: 0 !important; - border-bottom: 0 !important; - border-left: 0 !important; - border-right: 0 !important; - } - } - - @media (min-width: 992px) { - .border-lg-top { - border-top: 1px solid #e3e7eb !important; - } - .border-lg-right { - border-right: 1px solid #e3e7eb !important; - } - .border-lg-bottom { - border-bottom: 1px solid #e3e7eb !important; - } - .border-lg-left { - border-left: 1px solid #e3e7eb !important; - } - .border-lg-top-0 { - border-top: 0 !important; - } - .border-lg-right-0 { - border-right: 0 !important; - } - .border-lg-bottom-0 { - border-bottom: 0 !important; - } - .border-lg-left-0 { - border-left: 0 !important; - } - .border-lg-x { - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-lg-y { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - } - .border-lg { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-lg-0 { - border-top: 0 !important; - border-bottom: 0 !important; - border-left: 0 !important; - border-right: 0 !important; - } - } - - @media (min-width: 1200px) { - .border-xl-top { - border-top: 1px solid #e3e7eb !important; - } - .border-xl-right { - border-right: 1px solid #e3e7eb !important; - } - .border-xl-bottom { - border-bottom: 1px solid #e3e7eb !important; - } - .border-xl-left { - border-left: 1px solid #e3e7eb !important; - } - .border-xl-top-0 { - border-top: 0 !important; - } - .border-xl-right-0 { - border-right: 0 !important; - } - .border-xl-bottom-0 { - border-bottom: 0 !important; - } - .border-xl-left-0 { - border-left: 0 !important; - } - .border-xl-x { - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-xl-y { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - } - .border-xl { - border-top: 1px solid #e3e7eb !important; - border-bottom: 1px solid #e3e7eb !important; - border-left: 1px solid #e3e7eb !important; - border-right: 1px solid #e3e7eb !important; - } - .border-xl-0 { - border-top: 0 !important; - border-bottom: 0 !important; - border-left: 0 !important; - border-right: 0 !important; - } - } \ No newline at end of file diff --git a/docs/assets/css/v2.css b/docs/assets/css/v2.css deleted file mode 100644 index 8e936b45..00000000 --- a/docs/assets/css/v2.css +++ /dev/null @@ -1,15 +0,0 @@ -/* Chrome, Safari, Edge, Opera */ -input::-webkit-outer-spin-button, -input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Firefox */ -input[type=number] { - -moz-appearance: textfield; -} - -.v2-tooltip { - --bs-tooltip-bg: var(--bs-secondary); -} diff --git a/docs/assets/js/v2.js b/docs/assets/js/v2.js deleted file mode 100644 index 994c9a3c..00000000 --- a/docs/assets/js/v2.js +++ /dev/null @@ -1,170 +0,0 @@ -function getFreeSpeedFromMotors() { - let motor_count = {} - let modules = ['frontright', 'frontleft', 'backright', 'backleft']; - let highest_count = 0; - let highest_motor = ''; - for (var module of modules) { - let motor = $("#" + module + "angle_type-input").val(); - if (isNaN(motor_count[motor])) { - motor_count[motor] = 0; - } - motor_count[motor] = motor_count[motor] + 1; - if (motor_count[motor] > highest_count) { - highest_count = motor_count[motor]; - highest_motor = motor; - } - } - - console.log(motor_count); - switch (highest_motor) { - case "neo": - // $("#freespeedrpm-input").val(5676); - case "falcon": - // $("#freespeedrpm-input").val(6800); - } -} - -function updateAll() { - // let jsons = ['pidfproperties', 'controllerproperties', 'physicalproperties', 'frontleft', - // 'frontright', 'backleft', 'backright', 'swervedrive', 'controllerproperties']; // TODO: Commented out until all of the forms are built - let jsons = ['swervedrive', 'physicalproperties', 'frontleft', 'frontright', - 'backleft', 'backright', 'controllerproperties', 'pidfproperties']; - jsons.forEach((json) => { - updateJSON(json); - }); - - getFreeSpeedFromMotors(); -} - -function updateJSON(name) { - let form = $(`#${name}-json`); - if (form.text() !== jsonify(name)) { - form.text(jsonify(name)); - } - // $(`#${name}-test`).text(jsonify(name)); -} - -function jsonify(name) { - let data = {}; - let formdata = $(`#${name}-form`).serializeArray(); - formdata.forEach((val) => { - if (val.name.includes('_')) { - let structStart = val.name.indexOf('_') - let struct = val.name.substring(0, structStart); - let subStruct = null; - if (data[struct] === undefined) { - data[struct] = {}; - } - let subStructStart = val.name.indexOf('_', structStart + 1); - if (subStructStart !== -1) { - subStruct = val.name.substring(structStart + 1, subStructStart) - if (data[struct][subStruct] === undefined) { - data[struct][subStruct] = {} - } - } - if (subStruct == null) { - data[struct][val.name.substring(val.name.lastIndexOf('_') + 1)] = - isNumeric(val.value) ? - parseFloat(val.value) : - (val.value === "" ? null : val.value); - } else { - data[struct][subStruct][val.name.substring( - val.name.lastIndexOf('_') + 1)] = - isNumeric(val.value) ? - parseFloat(val.value) : - (val.value === "" ? null : val.value); - } - } else { - data[val.name] = isNumeric(val.value) ? - parseFloat(val.value) : - (val.value === "" ? null : val.value); - } - }); - $(`#${name}-form input:checkbox`).each(function () { // Update checkboxes because unchecked checkboxes are not recognized by jQuery serializeArray - if (this.name.includes('_')) { - let struct = this.name.substring(0, this.name.indexOf('_')); - if (data[struct] === undefined) { - data[struct] = {}; - } - data[struct][this.name.substring( - this.name.indexOf('_') + 1)] = this.checked; - } else { - data[this.name] = this.checked; - } - }); - if (name == "swervedrive") { - data['modules'] = - [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ] - } - return JSON.stringify(data, null, 2); -} - -function isNumeric(str) { - if (typeof str != "string") { - return false - } // we only process strings! - return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... - !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail -} - -function copyText(name) { - let text = $(`#${name}-json`).text(); - navigator.clipboard.writeText(text); -} - -function getText(name) { - let text = $(`#${name}-json`).text(); - return text; -} - -//function from https://github.com/eligrey/FileSaver.js/issues/774 -const saveAs = (blob, name) => { - // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue https://github.com/eligrey/FileSaver.js/issues/561) - const a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') - a.download = name - a.rel = 'noopener' - a.href = URL.createObjectURL(blob) - - setTimeout(() => URL.revokeObjectURL(a.href), 40 /* sec */ * 1000) - setTimeout(() => a.click(), 0) -} - -function zipDownload() { - const zip = new JSZip(); - let swf = zip.folder("swerve") - let cp = swf.file("controllerproperties.json", - getText("controllerproperties")); - let sd = swf.file("swervedrive.json", getText("swervedrive")); - let mod = swf.folder("modules") - let pp = mod.file("physicalproperties.json", getText("physicalproperties")); - let fl = mod.file("frontleft.json", getText("frontleft")); - let fr = mod.file("frontright.json", getText("frontright")); - let bl = mod.file("backleft.json", getText("backleft")); - let br = mod.file("backright.json", getText("backright")); - let pidf = mod.file("pidfproperties.json", getText("pidfproperties")); - - zip.generateAsync({type: "blob"}).then(function (blob) { - saveAs(blob, "YAGSL Config.zip") - }); - console.log("Downloaded YAGSL Config zip"); -} - -$(function () { - const tooltipTriggerList = document.querySelectorAll( - '[data-bs-toggle="tooltip"]'); // Initialize tooltips: https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips - const tooltipList = [...tooltipTriggerList].map( - tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); - - $('a').click(function () { - window.open(this.href, '_blank'); - return false; - }); - - updateAll(); - setInterval(updateAll, 500); -}); \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 0749a7a2..00000000 --- a/docs/index.html +++ /dev/null @@ -1,1269 +0,0 @@ - - - - - - - - YAGSL Tuning Webpage - - - -
-
-
-
-
-

Swerve Drive

-
- -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- -

-      
-
-
-
-
-
-
-

Module Properties

-
- -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - - -
-
- - - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
-
-
- -

-      
-
-
-
-
-
-
-

Front Left Module

-
- -
-
-
-
-
-
-
- - -
Inches (+x is torwards the - robot - front) -
-
-
- - -
Inches (+y is torwards robot - left) -
-
-
- - -
-
- -
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- -

-      
-
-
-
-
-
-
-

Front Right Module

-
- -
-
-
-
-
-
-
- - -
Inches (+x is torwards the - robot - front) -
-
-
- - -
Inches (+y is torwards robot - left) -
-
-
- - -
-
- -
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
- - -
-
- - -
-
- - -
- -
-
-
-
- -

-      
-
-
-
-
-
-
-

Back Left Module

-
- -
-
-
-
-
-
-
- - -
Inches (+x is torwards the - robot - front) -
-
-
- - -
Inches (+y is torwards robot - left) -
-
-
- - -
-
- -
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
- - -
-
- - -
- -
- - -
- -
-
-
-
- -

-      
-
-
-
-
-
-
-

Back Right Module

-
- -
-
-
-
-
-
-
- - -
Inches (+x is torwards the - robot - front) -
-
-
- - -
Inches (+y is torwards robot - left) -
-
-
- - -
-
- -
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- -

-      
-
-
-
-
-
-
-

Heading Tuning

-
- -
-
-
-
-
-
-
- - -
-
-
- - -
-
- - -
-
- - -
-
-
-
-
- -

-      
-
-
-
-
-
-
-

Module Tuning

-
- -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- -

-      
-
-
-
- -
-
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/json/controllerproperties.md b/docs/json/controllerproperties.md deleted file mode 100644 index 68f79cff..00000000 --- a/docs/json/controllerproperties.md +++ /dev/null @@ -1,17 +0,0 @@ -# Swerve Controller (`controllerproperties.json`) - -The Swerve Controller stores configuration options relating it how the swerve drive works during -autonomous and drive modes which set the heading of the robot based off a joystick. -The JSON files maps 1:1 -with [`ControllerPropertiesJson.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/json/ControllerPropertiesJson.java) -which creates -a [`SwerveControllerConfiguration`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/SwerveControllerConfiguration.java). -The values within here are **EXTREMELY** important to autonomous because the robot heading PID needs -to be tuned correctly in order for Autonomous functions to work. - -# Fields - -| Name | Units | Required | Description | -|-----------------------------|--------------------|----------|------------------------------------------------------------------------------------------------| -| angleJoystickRadiusDeadband | Double | Y | The minimum radius of the angle control joystick to allow for heading adjustment of the robot. | -| heading | [PIDF](pidfjson.md) | Y | The PID used to control the robot heading. | diff --git a/docs/json/devicejson.md b/docs/json/devicejson.md deleted file mode 100644 index 0f1c8254..00000000 --- a/docs/json/devicejson.md +++ /dev/null @@ -1,13 +0,0 @@ -# Device Configuration - -All devices in a swerve drive come down to a basic set of fields. The device configuration is used -to store and create those devices during parsing with a 1:1 mapping -to [DeviceJson.java](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/json/DeviceJson.java). - -# Fields - -| Name | Units | Required | Description | -|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------| -| type | `integrated`, `attached`, `analog`, `thrifty`, `throughbore`, `analog`, `dutycycle`, `cancoder`, `none` for Encoders.

`navx`, `pigeon`, `pigeon2`, `analog`, `adxrs450`, `adis16470`, `adis16448` for IMU's.

`sparkmax`, `falcon`, `talonfx`, `talonsrx` for motors. | Y | The device type which is used for creation of the Swerve type. | -| id | Integer | Y | The ID of the device on the CANBus, or the pin ID on the roboRIO for certain devices. | -| canbus | String | N | The canbus to instantiate the device on. Only works on devices compatible with alternate CAN buses. | diff --git a/docs/json/physicalproperties.md b/docs/json/physicalproperties.md deleted file mode 100644 index 819c9b91..00000000 --- a/docs/json/physicalproperties.md +++ /dev/null @@ -1,25 +0,0 @@ -# Swerve Module Physical Properties (`physicalproperties.json`) - -This JSON configures the physical properties shared with all the Swerve Modules. It maps 1:1 -with [`PhysicalPropertiesJson.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/json/PhysicalPropertiesJson.java) -which creates -[`SwerveModulePhysicalCharacteristics.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/SwerveModulePhysicalCharacteristics.java). - -# Fields - -| Name | Units | Required | Description | -|--------------------------------|-----------------------------------|----------|---------------------------------------------------------------------------------------------------------------------| -| wheelDiameter | Inches | Y | Module wheel diameters in inches. | -| gearRatio | [MotorConfig](#MotorConfig) | Y | Gear ratio for the motors, number of times the motor has to spin before the wheel rotates a single time. | -| encoderPulsePerRotation | [MotorConfig](#MotorConfig) | N | Encoder pulse per rotation for non-integrated encoders. 1 for integrated encoders. | -| currentLimit | [MotorConfig](#MotorConfig) | N | The current limit in AMPs to apply to the motors. | -| rampRate | [MotorConfig](#MotorConfig) | N | The minimum number of seconds to take for the motor to go from 0 to full throttle. | -| wheelGripCoefficientOfFriction | Coefficient of Friction on Carpet | N | The grip tape coefficient of friction on carpet. Used to calculate the practical maximum acceleration. | -| moduleFeedForwardClosedLoop | Feedforward between `[-1, 0]` | N | The feedforward scalar to apply for 2nd order kinematics. If robot arcs while translating and rotating negate this. | - -### MotorConfig - -| Name | Units | Required | Description | -|-------|--------|----------|--------------------| -| drive | Number | Y | Drive motor value. | -| angle | Number | Y | Angle motor value. | diff --git a/docs/json/pidfjson.md b/docs/json/pidfjson.md deleted file mode 100644 index f46d603e..00000000 --- a/docs/json/pidfjson.md +++ /dev/null @@ -1,27 +0,0 @@ -# PIDF Configuration - -The PIDF configurations map 1:1 -with [`PIDFConfig.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/PIDFConfig.java) -which stores information regarding the PID or PIDF configurations for the robot, such as module -velocity & position, and robot heading. Not every parameter is used on every PIDF configuration. For -example `heading` only takes into account the PID. - -# Fields - -A `0` in any place basically disables that portion of the PIDF. - -| Name | Units | Required | Description | -|---------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| p | kP Gain | Y | Proportional Gain for the PID. | -| i | kI Gain | Y | Integral Gain for the PID. | -| d | kD Gain | Y | Derivative Gain for the PID. | -| f | Number | N | Feedforward for the PID. | -| iz | Number | N | Integral zone for the integrator of the PID. | -| output | [Range](#Range) | N | The output range for the PID. | - -### Range - -| Name | Units | Required | Description | -|------|--------|----------|-------------------------------------------------------| -| min | Number | N | The minimum value in the PID range. Defaults to `-1`. | -| max | Number | N | The maximum value in the PID range. Defaults to `1`. | diff --git a/docs/json/pidfproperties.md b/docs/json/pidfproperties.md deleted file mode 100644 index e2b41c17..00000000 --- a/docs/json/pidfproperties.md +++ /dev/null @@ -1,13 +0,0 @@ -# Swerve Module PID Configuration (`pidfpropreties.json`) - -This file configures the PIDF values with integral zone and maximum output of the drive and motor -modules for every swerve module. It maps 1:1 -to [`PIDFPropertiesJson.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/json/PIDFPropertiesJson.java) -and is used while initializing the `SwerveDriveConfiguration` object. - -# Fields - -| Name | Units | Required | Description | -|-------|------------------------------------------|----------|-----------------------------------------------------------------------| -| drive | [PIDF](pidfjson.md) | Y | The configuration which will be used for the PIDF on the drive motor. | -| angle | [PIDF](pidfjson.md) | Y | The configuration which will be used for the PIDF on the angle motor. | diff --git a/docs/json/swervedrive.md b/docs/json/swervedrive.md deleted file mode 100644 index 6a09a2cb..00000000 --- a/docs/json/swervedrive.md +++ /dev/null @@ -1,20 +0,0 @@ -# Swerve Drive (`swervedrive.json`) - -The Swerve Drive JSON configuration file configures everything related to the overall Swerve Drive. -It maps 1:1 -to [`SwerveDriveJson.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/json/SwerveDriveJson.java) -which creates -a [`SwerveDriveConfiguration`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/SwerveDriveConfiguration.java) -that is used to create -the [`SwerveDrive`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/SwerveDrive.java) -object. - -# JSON Fields - -| Name | Units | Required | Description | -|----------------|-------------------------|----------|----------------------------------------------------------------------------| -| maxSpeed | Feet Per Second | Y | Maximum robot speed in feet per second. | -| optimalVoltage | Voltage | Y | Optimal voltage to compensate to and base feedforward calculations off of. | -| imu | [Device](devicejson.md) | Y | Robot IMU used to determine heading of the robot. | -| invertedIMU | Boolean | Y | Inversion state of the IMU. | -| modules | String array | Y | Module JSONs in order clockwise order starting from front left. | diff --git a/docs/json/swervemodule.md b/docs/json/swervemodule.md deleted file mode 100644 index 7d8393f0..00000000 --- a/docs/json/swervemodule.md +++ /dev/null @@ -1,27 +0,0 @@ -# Swerve Module Configuration (`module/x.json`) - -The swerve module configuration configures unique properties of each swerve module. It maps 1:1 -with [`ModuleJson.java`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/json/ModuleJson.java) -which is used to -create [`SwerveModuleConfiguration`](../../src/main/java/frc/robot/subsystems/swervedrive/swervelib/parser/SwerveModuleConfiguration.java). -This configuration file interacts directly with swerve kinematics. - -# Fields - -| Name | Units | Required | Description | -|-------------------------|-----------------------------|----------|---------------------------------------------------------------------------| -| drive | [Device](devicejson.md) | Y | Drive motor device configuration. | -| angle | [Device](devicejson.md) | Y | Angle motor device configuration. | -| encoder | [Device](devicejson.md) | Y | Absolute encoder device configuration. | -| inverted | [MotorConfig](#MotorConfig) | Y | Inversion state of each motor as a boolean. | -| absoluteEncoderOffset | Degrees | Y | Absolute encoder offset from 0 in degrees. | -| absoluteEncoderInverted | Bool | N | Inversion state of the Absolute Encoder. | -| location | [MotorConfig](#MotorConfig) | Y | The location of the swerve module from the center of the robot in inches. | -| useCosineCompensator | Bool | N | Whether or not to modulate drive motors when pointed in wrong direction (defaults to True) | - -### MotorConfig - -| Name | Units | Required | Description | -|-------|-------|----------|--------------------| -| drive | Value | Y | Drive motor value. | -| angle | Value | Y | Angle motor value. | diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fbacf711..34bd9ce9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=permwrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/networktables.json.bck b/networktables.json.bck deleted file mode 100644 index fddee201..00000000 --- a/networktables.json.bck +++ /dev/null @@ -1,442 +0,0 @@ -[ - { - "name": "/SmartDashboard/AprilTagPoseSystem/AprilTagPoseSystem/Std Vector Factor", - "type": "double", - "value": 0.1, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/AprilTagPoseSystem/AprilTagPoseSystem/Std Vector Radian Factor", - "type": "double", - "value": 5.0, - "properties": { - "persistent": true - } - }, - { - "name": "/YAGSLSwerveDrivetrain/Field Oriented", - "type": "boolean", - "value": true, - "properties": { - "persistent": true - } - }, - { - "name": "/YAGSLSwerveDrivetrain/Has Issues", - "type": "boolean", - "value": false, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/BabySwerve/BabySwerve/Alliance", - "type": "string", - "value": "Blue", - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/DisplayValueSubsystem/OUTPUT_ANGLE (°)", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/DisplayValueSubsystem/OUTPUT_BOOLEAN", - "type": "boolean", - "value": false, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/DisplayValueSubsystem/OUTPUT_DOUBLE", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/DisplayValueSubsystem/OUTPUT_FLOAT", - "type": "float", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Config/INPUT_ANGLE (°)", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Config/INPUT_BOOLEAN", - "type": "boolean", - "value": false, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Config/INPUT_DOUBLE", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Config/INPUT_FLOAT", - "type": "float", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Input/INPUT_LENGTH (m)", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Input/INPUT_LONG", - "type": "int", - "value": 0, - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Input/INPUT_STRING", - "type": "string", - "value": "", - "properties": { - "persistent": true - } - }, - { - "name": "/SmartDashboard/DisplayValueSubsystem/Input/INPUT_TIME (s)", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleRobot/ExampleRobot/Alliance", - "type": "string", - "value": "Blue", - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kP", - "type": "double", - "value": 0.1, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kI", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kD", - "type": "double", - "value": 0.01, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kF", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kS", - "type": "double", - "value": 0.1, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kV", - "type": "double", - "value": 0.1, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/kA", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/IZone", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/MinOutput", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/MaxOutput", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Reference", - "type": "double", - "value": -0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Tolerance", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/FeedForward", - "type": "double", - "value": 0.1, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/ControlType", - "type": "string", - "value": "", - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Position", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Velocity", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Effort (V)", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kP", - "type": "double", - "value": 0.01, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kI", - "type": "double", - "value": 2.5e-05, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kD", - "type": "double", - "value": 0.003, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kF", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kS", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kV", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/kA", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/IZone", - "type": "double", - "value": 3.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/MinOutput", - "type": "double", - "value": -12.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/MaxOutput", - "type": "double", - "value": 12.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/Reference", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/Tolerance", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/FeedForward", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/ControlType", - "type": "string", - "value": "POSITION", - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/Position", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/Velocity", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/angular/Effort (V)", - "type": "double", - "value": -12.0, - "properties": { - "persistent": true - } - }, - { - "name": "/angular/kG", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Max Velocity", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - }, - { - "name": "/Shuffleboard/ExampleSubsystem/velocity_motor/Max Acceleration", - "type": "double", - "value": 0.0, - "properties": { - "persistent": true - } - } -] diff --git a/pictures/AT23.png b/pictures/AT23.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ac41215327ff0c735fcce760abda8aae1b758d GIT binary patch literal 5630 zcmeI0`&Uxw8pn6u%1c?=Nh{RVWvQ((^K!gYn&~u?p7D~IqNI2s#dO9?Xc>x_<)x|7 zHH(?jL2Nvg83e^KG_ehs4a_-;jCqehO$QP$*a#t_abBCh!j?bmXYcp@W$*Q@=llJB z-hKU4_{q&?yUZX6+8h#m;xq&qf@edcO(tN+!Q)jVxNX3lJ{bftdJt1!VT26~3xptM zwfSxG4%>k*R{;%x-2$936J z<|ApL#jSB$mrgCwZ*q?sWhT9l%CXO#~AjVhwe;974u+6HGG20^PRQ%iPPKu#K;_A$L%XO9k%coki zN9!4BCPCx2Tg|$}_1haGVrow+VMZbsEeg;V&Cbq-A#pFgg;ll71KFSY$xIZJ#LbG zGl(g3LCKM1Ok~Pk z$A3nYoo>$#_?z%^AN|#cjqG$oAU4Xp{FM}aS9)}+ckJ-}Z*D7qlh?bJTc;ufORBzz z=@(_@1DZsgCwI1$l9C+fll7(v%r-FMvgMPpYsDu9gYvR-Tx*G-cv9!{(A zY*WvzX66~I3Di?}I11ZSSIa23zR!vwuBefcHXN4yeJv10p&BSN+{UxH87mrm81-Na zoNH~iFkCe`O4LipJ;tm-#m`9e!^|N@7}DLa0pjh^Yehr_YY0sS`I4Z7UD3~D+uPgw zqPuyc&LBks*UM+ugPdTXgGytS6vGP{e;#0uiau<>W9EFA8DZ<@+Nb*;MzLC3eG-J* zKwuOV%PE%>Wj5%hagyk`=Rz_C;*Xa%dCGxvw zkn`=#%uN3P4LPlv>g$WndF!X{kNQMADJ3HX@kZ;+{MjBEj;p1Xn;~F*Uqe-80D9SY zbq3`a0nh$^pZDLc9OFPxMG`)YV7;m)@;uX?HaACE9$QRe4Q{#h9kE=WPp;U3WW+yQ zr2zIXJ2x4R4Ff{hDPRJJO}171;3IAN0?7#VNlr^F)~5PvCrJY+TJoZNBNTLnl(I1Y zZ>r+Y+ldT`L;}#77f}s0Np&5jkBb=cep77oK z=C-&}(mCDpOCJxNTzAT~@05b@RcqX-lFsTdX}D0t?j+No4UVYHzOTB^Aq2YwynaFzlbbJ+0-ioJKviJl}~|4z_bFTYni>Xo4fuIc^Sc4fr0HMD-%CyTwpJ10xW-L0B zUY_h*X~cwF*3N7KDX zo!#DYyb_)Rr^O?e=HBF4myD>Sma@sStyu$yxXT`rYrxSLZNSLuvUk*oF4m0byMU=~ z%-#;&da^Q~vOpZVr!_Smi*?`Wl6@r5(50j3iQlKNBz9Y@W5XK6hHH9*D2D3Q0mRI| Qz-a;s`6~PbBPgl#|Lq2xv;Y7A literal 0 HcmV?d00001 diff --git a/pictures/AT24.png b/pictures/AT24.png new file mode 100644 index 0000000000000000000000000000000000000000..25dc78008810595eb93e5e8504a1bdc54f4661e1 GIT binary patch literal 5263 zcmeHL`&Uy}8a*jE5Riw#Rl+k`kXdb+AqoX65k=_`Rw1^v3nXOmV${2f)bz#$zTc5$tYM#0+X760aA&S>Z9^ z0Pq{^jcL2!7@3RztN;L1p`{^YnHVYndwruK!at>^EKMJ)^Lp`+HJ3Hvcljzi_^o%& z^bphD{uuuXVLa`&$Ak7UuONKY1^>>jy4rSbc+?#pKk(bV{N2_WC;dl$8Ba9g49YOZ zgnyg5GA$(Vw0Nvd)vs!97oxTB#cPu6o`f8Y6#)9F&krB~pf+V#1F$dR4FH_qf@9l8 zII9{DyA!^!+m8Z(oVz7_ z(qJ8!Y7(OVqV%Fs_oZ~jk_0KL!}l}t%YwgHt}Erl#Jb+z>NUsS##W^^@HV{*mHDO1TQUO2a(%2L}?U`O6mX+ z3WzSYYF(MZ*}l1R;k8Ox!b(OV0lb}c=}Dk+PBLQW#H%LP&rN5AM@B{}*4C)BkkCS{ zQk*yUpe8Rr{}!Xx>t6ngM}5UhKOHXLH0t+z^KG2G+cud7{ZgK4!s`bf&naw;XoNVu z#^1M>mF$AFcR~K>g3PLd;h}u5Lo9ikeE{`lkVM;8xbUyOJcUrdpVhu{Z_hAidf-`p zZZ6i*(UD-zNu*fCx8T84!L-Y#m;RlxzMFOX`0tSbcG&tiq%RBp_$fC;AT%3 z-}UnJw6)g%6eP^G@t>ccKjVqPoYQ2Ur^p80|8r6wmOvo%I`L1&btkz^b|-tNBwTse zh)-|I%~Ppo*MGTmN_Zue+0U>G)z6g3*>S0iqIz>IYT(ta#MZN$My;gbnAuohz%K4; zLj;?mQbRlcNp#s|AZl<7&05U4E^)94fV|#8~c_{ zGF|sHPVrS-ire+4+2h(p<`iH2-Z31`v)rV+VyF=KqTcjFfvMt^|BU9DHgrofAXHK0 z^BdxHM`msx-HjDCsY=JS^o>w39M&pCXGYi`Ida5ZBoa;EfBub}X~>})S40Y#fJW*` zVC%{R47*`Ma({jX&aimfLYoXH^F)XC*Q%>`Tc`d&+asTPf4$sd7f0@#GcC9aY@_L4-z$E?SR0Zn5V^YDT^1? zj%FX!1PkI)Q0LX!NrrSM@R*Iprp^3Gaj5~i>cJZOc)0%Yl?;j ziz?WX5seP<6W@J!MW|Cxim(L#^a61*P2Y?sYP`I?FG5t4aVsM(Nb6HetOGYD)p_f$ zUP$pEzEbqvuaD)vw$?;)?4gR=!y$WiO#5~&Po%q(#-~HmxSnv+G3CZQ9F%$_jta_8 z=!8Ry;I2b=!nPm=(jLd*=`h8@VWftm!70?a8aNGR>Kn1wr4?>OWI4pxnn5!(ir}Fe zK_afl>T<8?%LgSW-q}q*L@&A?!D3`t3O=^)?o@NOZF$RVBTXwhd0hFqD=deAs1FDc J-6x2Z{{^Fu%xnMv literal 0 HcmV?d00001 diff --git a/pictures/AT25.png b/pictures/AT25.png new file mode 100644 index 0000000000000000000000000000000000000000..277d29859164a4f5237b2f87b4cf57314f353225 GIT binary patch literal 5971 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 OgARNXgXKmg{`g-88DaJS literal 0 HcmV?d00001 diff --git a/pictures/AT31.png b/pictures/AT31.png new file mode 100644 index 0000000000000000000000000000000000000000..e9e5eb492d0329c36bec65dd60a6216856ca0f4e GIT binary patch literal 5829 zcmeHL`BxKJ8ZA&n5E_M6KoIC|T1N+2L<|ZNgD5h{qQe>>i#UNpAZUlM1%eHN_OL`p zMUhQx1x-u{LfAssA~4M&-9kttlBmH%2pYDqg^($x=gj}0hacWo=hZoNs?M!@?{~lV zi@&d@zV2>a008v8&Ul;$fK58!+PZZMcqgmp>3;B{fj{r*4$xnkPk@`vQ7|7E0MwAT zt)jKSz0RdGp?Cl=OjutULX2fH0PKQ$dB6ga5uXH5>K?8|&pT1S@9k)-3@hEB-Hh$8 zHoL&CzAd(A27X(RzvK4dnVr^0s?Kf}!FpdTX@{A?{B3sTS!ZWk?In&J#9>6LJ@G`$ zktylV?@IcGT)1Pc#1a3B5W$0E!Pol-as%(UgP(C^W#StEK(+m{833%VZG?#C4RPiZ;pLz!Lpzso~KeSRw2<_zSrRE$Xn)o$6C`_1$$g1Inf&8hpQdhmcs+Z zkSa^ZT2_*L8Hi?8LlPxiO4f9U=;ge;+&a(vwHvF0z}@ z)y|)jvd(~H&DMu?f4e^OsIetEvz>;c*&h6xWeRfpzhU=FX%k0*rEn5oK74YDiuiWiAeX~!#DcfECZN6VkgF4QoInG?!Du?{s-qPI^LJMNTX=_fN z^(hUX8lpAPp{3VOPdVHKZk<3O1Y(>;p#$5?X>A(%QhVXfhAzH%U4(@22tyGzD3hER z8rNR0ScFE#S}J9V(V4cB&vvw*_vrkFv306|F6-AOM(x&${EMJaP+xtD?$vJS(dpiu zOA;+|U6}$Mj{VuB4xvf)NsfJhC~S9O-5#A0ZPfmt%_zes0Sbn~O~q>BxP)CDWM9`n>Rq>LV<7-1rK*+za ze;j(I#j2nlaAZ9AKPig-5ciN}?>N_-^iG0AqcJC&aVt&*{#aTW@pIF$l-Y}fh~RWl zaaK`&kE){sHrA4YUvY4BAuhxoS5DOiT7=s?lr@8@xr-!)R{51D9>4#3T|m&aCGpDY z+@KgQ`Oa|HUc(Z&oCk(6=ANvn;$OM|$Q zYi1DKaH&gDrKRJhn~WQ+a=NmN>A<^ok-lIK<;#bpi{gdWjizwnLiw(uyZ=b=6H!dnAM(UF zA4I_!099+n`fgNT5_xo}2C_CMut*ZOncrNWj5o>`jltAyv`Hn)`2y(m-Lv&lPK!;X zIUdi*8-eR{P;ZR)8lt?{qfI}`e9>XMjo#d-g(W2zy=}7am#jzyCAXFWYa{Vy%@+yl?{hz2_jB0vl66KxQ+Z5nXzoa=}$bHn1kxeqSp@8afh@}dRJ#e{ezA2f*DxXLP}>^bCp4E zZ%WPePayaEcIZqe!1$!|QRm7A7x?6in2W1Yr`9Hi1y-c1WhHNKfzSjajJEYQw;43u z5Xe{L4}oCB0f$p&P#|-83?rzD1O2-hmzv7B0#1%>L_En?+M8G>Xx!WGCIkCUJAx=F zike(qp5qcC{u_c=p%<$4IQ~>F1&&oHz1 z1W8(cYyl;?s&uEcZUROTyp+rrLQ}HRVHY@KgRLR%T1PUcx20a%e!tz#PzQ+3gPg|w zvoOlGP+GA=dDa?aUPneac%Hd&?DC~!(3t_88z(#82&jn#|0>{h%GZPLj>`NG3ps|{ literal 0 HcmV?d00001 diff --git a/pictures/AT32.png b/pictures/AT32.png new file mode 100644 index 0000000000000000000000000000000000000000..4c0dbb95a3bcfa55a55bf061e32762ceb3d43c58 GIT binary patch literal 5560 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

2VW(skbI4kUJiGV(+Be|6YXW2 zi#;(X(AC)7Le2<+Cq4=9AALFLzQ{hIOboJf5khbMPMQh^+LBzuS>+`TnME7)6G+Z- zp%oRi)riBmM`Sf*jj}mHh*=3|=-F8bn%wb8Cx`SeaM`Ev1Dl*h5qxWrM6Hu=`)QBa zJcjlN#QNMtNX8F#xQUahoxna`o!}T8mx0z6MEK{cmw*DvJ!Ih*0aVpyqEs7!==C@X zXH{5d)p$j&SV4X@s#RD1U-Sa8k^k)VXylPzjJpsM7Ojz~?5_ z52Ii;eKi$U9&NGY%;#O)r7FJ#LJN|TvZtbExH}nWYPFj4`LpIzB`VR5e)T8Fq5a4y zfm_ah%lplhiH$yCTXrf>lf#gp3Sf;8Lk+(3i36<&>2)5)j4^|8C4T4FBK^trkf;=0 z$zj5MiXWCw5)7M)6$bWz%0!Te%>+NTj<5 zHq@PROqFL;m2h;*F(4AJA~wOIK;9QzWyVYEy8(=8nb=)S_UiSa<)$X%_NAPs4O^2f z`Q@fPQhw%?()6*NNGRiX2Y*>I@S}!z_=3yRa^RTl-QZEj{l=?L2+|68upTdYJfD+J zPAs^_yFa0F3DLvfG{*>qq50>$?>K4(lRUtyHh*~khtEOK891U)QBmD@zUk-zg9*Wb z$4yN2HBryTH;QI21U);}+UvkPuxP;(bM1H-%akiqZi%r6p1pt4S$`ycO>J!tXubsR zji4d0wELFIX|A$#TuX8!HE1SKts2YqTlDnU#l3ZQW6m{J?v&5N`yE&(hwXA1xj<7WH{3i%2(c3`0e8UE)<6zud1?u?yjb@$6dRd2IIQ3i}}$r~pN58p}ULfW(1#%gt3xrinWaJ=!^mT3cx?=cy}4$4AgLrAI|^ ze_1Ow3j5c|A?3AehI2zP&c-)P3z;yO!APV^xFsKKYQOxB z9BH2$@@q*h#^l}V&dac?S13%J#nOFy;0#AmR#mK#d zU6T8Y!2UubG3Z$5XVZnUeN5t`ivJF|MQGbz?XbC7>raqGqxgxVViHQ9JEhz8 zX-30>NtdNGmX5hT6))1!EDaZ*D7}&{qq>vw>&wqB@?lQ6RJ>Svkj`DTD7#%Oc7_&CS)5Qqd7 znZJonukfQ{Zj;vQ0;WO9fT0*_GBbhHE;d&)2|Ih$v9Y0fxKA6t|0+ZSDxnykzG&k* z{6OGrj>z6=Ab%5$QEW4?6JEjdsOqaFKRkA>?rM?FJ=OM|O9SU8^0M*Iev4--Eh25& z;=$J?o(3l;u&CYM4uNg49A$iwQt5rUAD{Or=SuRDV<{jk z=^5X`tv&CuW{SDCoVBh({VGgI`*l4g!2T2ckz3H zc8Iv)!{?K;vn>}qaH#x!rg>!bx|eq+6uk;q%q?f_!@G-=YLf-JjjB_Ga|%preN`4E z)HuB7Hs&~^M^Tes7#24is1LF}B3P)zjABeUq^sxiuP@eG;7ELKlx1S)McK0|>;6BKrKDR2zs?X~Q^Zo4%ToYN;NusqmS?{k@40I{W7HxP1y7xngw@Fp+CB zXmT_SAAsS^g8Vxw8G=CayuY_CH(x>Cx<4%EGHj8&_9#EN?C-6{3e zMz<2nUCGQMLa(H+Im0U)MXLf37hW_z6JRl74d!DGk`kV~G&Y8zVH4-(8rrGd-rZ5e zi?1w{o4j+^#CQH8JynpB)N0KSyh+OjH8r&ZvpbO*`?m6*a=`nr5+g|_7GRoLvp@x)iTYIofS3p}bRg2o zKu_NVsI)-CMhglY2(n=V=)j}XXz~2!%-px=O?igoe`*M*@L8OTf8E6=Y8!V_@j!sd zZ4wa?86>0D#$pD@Nrm9gze4;NVrDSK%zBujGC~7o7E!`T5Gyd^-6f-#vZKahMvcFh zm_A91NI8J@&C2vcY8w+TRiR5o!mcv+=+g~TX1(2hF;So?GUTt$*=|wW9fQ)}3Qd~j z68fC&>y8xbZ|B-Uze$~4L>cfDD6pxA`2+d%(hV5mZfNHJ7=G#1N3J0^4I*YfJ$ZUI z8QqJO?=&M`=j+{jyXxGknD=m04=T$P;qy30?EYVb+B#kvC-&d_(gV*gaa|9z{l8o!o~xnOFgSMsuvU; z;K(7*B(X*e?d?fm;8()r>NZ}eBS(e3mkPt^Vka9}%;^$m&wafkX27?$JQc{!EOm?1 zDWgPbN}?nvm<3`@sOR0xK~F}RKLwyV+~&5!>Eqiu7qwX>Q>Hd^tEIU#NbppK5JCif zFG66|eYz0mxgFKtV|>EKTG^wu;?}j3`PhK>QDHtAdr;1nX)cw=(cYaqXu+2B?#|=Y z>W+knb*y2nxo_=h%AtHTTn|sepXfQpG6{(`?^Z#{wj}<|kFU(g&dMU_?yQ0?F)-V| zPnuJAt-<-_4-@eNQzn(gWQj^6eU4q(S30I}ow`Ix%CmrP}AM)o?Cqo zUT?#!m?0m3^ECIbo+8k;U84X}rxAFx%kaL$fs+zo`D7m6N&nhgIsG~?L3&Z-$~|u2 z<&_CkIzR`)#Kkpgj57L~N|G57L^?@5w8kz#gGH=|-6NIWXr!Oa(M%(%hAkkL9#Y#? zs$ipyCqt(q870w+*rtGJV7-Jued3ALdTJ94McgLnBpWhd#S0q|bdW_10qR)*cnc_g zfk8o7;2*KKj{^~N@E2hJ20^&;Z37UG6c=;604OVhcTU}b=5?Z*x(aoJxRgmz-Mh&K>s(y_qX ze!t;C?fiPy%~LdMtMt9JcQblh|6jLu>1HZ*8aH{DA=wvx6KwvkO;uq3yt;Pz*F=;` ziY{7_!PrhMyz^wjvLS?2focxoG{M8s4NA>_ZuRP8I;|AWh_M4Db!LZ_-QSe=|YK!_F6~g^;CzH%w%c?&0!8MbL?>%0S zvaKpGI-TkJt!V=AApr~vcfK3{oGKG$ya~j=I!iov*j$#L{vEWy;L{e5>v>@v4oOoX zrAnp5MVT?!dmv{yA|q?Hw4Wl(VH`#Ou>hvAqrTfSH((E%{O=m03LE-mFm^3naqq$f z(VF`Xl}vgbtnIoNn^QY_EmduasXJEVQ z44O^A(TJVP4=YExw5q1YFD|CS>*WZ<&{4BCTzM7*!?MkPWt&%P2-}+O3V+s2$KA`X zL2a$28;oZ(ATb~dERPO14a>Lx1)DCuw0A2Vl#W0mZ+iLg9DNq($LMwc7m?cIu{*>% z=4xT7eE%xzt?g|8)qvfsZPn-nv-UTB+ga@R3QmU4)-n!I)c<8htX_Am2A~|Y0lD36 zYTaD>h=OD9v-l|mCB|oty-U|MSL|-s@Un!M<7zF$wX1jk{f=dVvtYN5n$?k563)dPe?>PG<<0Epjf*=hF9}=*GTF3+*L9kjmn)q+ywbw7@8lle+Y_${DfZXA?wK(lD%{lad(CU}1G9?gPnyI~Y^yLFF79Gy zjxRRNGVm1HGqCp?DgDA4565_WD0V0V6THj7GK$sq=0HNB&>cTy#bpUYHU%b-s5(ds z9SVA;FFUa-wp{++8~%NU>LT-svdb^k{o@T>r|}+=Qk79E@2%<);aRhQO6HMYIq3!f zaESG1(0?ODMq!H$SqYm|MT2dsRqoyJa)38I=z&X+n z;m_tu>i@tDtqR#jElgVZk{}S)R7KY%NT0imBVAd63Y0)73u=CyHTz|vMDeI!SoawM zZIgd)xHdx|o80lV<`+Qi$(X#ba^LIgiyGU>V;hXGHz*zXhh6-i-5HfwQXj3?0W`sa z=sh_6|9{PfxzLEB2Y9sO05|uHW@jKY7PiCA=qkJ0_nAr(O)Uv?^LQI+tCwo&$pK=1 zqf9K+iYBj#>&uqpSk8&~Eyw#UJB<4=r;ec6Ep4au)yl$Uoa$(|?#X`Kmuf7r1m?6K z&8$>uE`o*AdLRs@EMXLRBy?@bQDUNpuEOo!FEY>gi2p((@F5vPJW!bQ@G8<5CKg&s zN~)^arRi-?AjW9im^O1u)Hy@M)CptliY;;8*+6q|XATuQs9Ded$;A32CNLIT7= zzVlDVYx!y;l5^AqBRZ7i`T1 za$%p18EAJexSPMRYZg4k(n0FT;=y9@R%Wf_DYoI zOZAB94KPI4Dr3OgEEop;^geXfd)Nh&fm^vj{Q3Nr;f!}-W5YNglUH@G?dd0#tok>*=sv)|HV8+J$v35elnWwN-y|dxdyfJ9<{)(^rkt z;b;%ZT;&AVZ&RSGXN5`Xd=1{|}QrkLGd#>NkQ- z)0C+^GK1|v;fKw5cJQ1hiNqbb-b#W>f7fqefz3hufHuF|=L|e8WANliqYUYfJb~r( zKw1VQcu-Ui&Q_qw3RL&>+oTDMuhv+nI6Q3xhLF`aG+cOO0z@_Vrls7o-lC?eiVqZY z$$tG;G11oIgyfiB$_O^D4j^c@rI~@dGn7mZtOi@QI!z3vLjecKzmiE{6%06jTn-6_ zu@<7bA4vMnlWoEEDpu#cCKq^q`Ev5_-kECY`@t?li_-DSu{Eh-_f3#cKq8w0LKwxD zUsEgqjxviOT4=uZ?XC#Wm+~OzgB*6NT^lu#Sam6>*+U)Nq?r8 zy>4M8u&|_kwEAmu9e*q)jTFAmMe)~(8O*SWp&SYnuo*9C#Rz}iRR{}2OES)^0++j1 zskI`}b!>^JEver-ZflBP=oPz*?ruX{rLjneWi1XO@^&QPk1A5c(sMbr$~k(Zz<}=) zrr#h?9lT%iJ8;_s8}3^0$aDv;NydZ6($S&t`t++Tk-h^%IRkv3JoXPeGcBE=$}fh{ zoYWb1H3Bds<}6COBpdYCxo=$Xlx=&I&Bmq*wnn0RPGf!dyZpXR#+$87X7tIEHV|U; z*@n$5Dev)*osa93%r*kj`Xq>54&naCt z8k^$}tgu+CGitdzaXJEn_F~T0n#sfY}P>TSKX?`#CBRmd*5(nGjE#r8NBE^PvT?zR#vNmWQ_QLcTk8l)g$4eW)9mVD`24CMJa;2_zFuF0o?t zKO}!7bQ{Y4DDyVeJ1vp%#^s?b`1GQ5@@6;n2XI)j2p?by~*OZqRG&bXZ^E~rjroLJRx@0ewt8zEjJN6=k_m9xP!}>fozEgh^+pT zykTF8C)R6%jra|u3S3&*vr)SRvI?}ArH+g_jy&#(e5XS9-2|bCVz3>Q`C(F|JKIV+ zW5FDX846w7UE9T=C=ZhVGu?`v(I0D_Qc5#bZ+O2W+gu8)=&jvN<_|Y&@9}c)T|Rw| z`OKbOEB(QQ;};bnQ6?sG);<>;Wm|`c;26_r zq5l9e++v2c<9n?2har?QVpF#Fvk`gNt2%{&TsatAq!6$g;ob>OS(_KF`@AY!V$9PQ zye+yHI@*)>YilzPyzQu4UUz0`KuM}Tvm?umjx=?bzjU#ZXd3B(>k9C#zDL)G&?+*w z#6Nj_Htfzn6-O`Sp+=Mx_-Jaia|tI^wOSjKeRvsXjM@Y1rt|>l#Po&vr=7L5K#m^FZZ!u8 z3yfPOk?jUhz}9@T9rVbH6C6K5jVkr0#Hd zy-XV9O&fA3c0M@I?+qP8(q^U`eoa*j(TL)LEzB|~;3F(!@{rPc=)nIo-Qv2zHb}$t zKH2bJ0Qmm)2*brg&%fztli@;@80Z7yLq3QMms$B&+{H@<7+V{PgdMgB@~e~CZu*swe?588{~gPq%0YG z*GT{Ss^y1NyJuhLL>Jn63yrku(9_f3`sxfysV;AGmL(5SZi>H_-H7v=+IX{AGWU53 zgO-lTqMDqSS0}v$)y65ttlvZNh7-=3ker|0CGzpo#?45WBSE6EO&b*2Y`Wk9oFEVb znCrX*b3$HUhIp!R{NG#KRCEm|lf5lGc-yJ{_j?W$G8+%av-yZ8RB2whs0IWk7>-Iz z!btiT81!!DO`F1Lkw^JV=_0(0{}AlBNq9ZUMNxxskRl*C zUW)$aQ%!^^Dk-IJ8skqBa)iPQqgwiO>;zHb_`M%znI2Bt)fQ5|dIh13~rriy!DJrYadQ|yQO zy@^__{G%o=1LiLu1E)$2DF)ZhSv_E-Q!~v|oWBOxL3zHY`S=t8!=1>i0N2(B{`VrG z7d29u9jdm$SS4j0POF>gV({~)l^q>x>9glth#-FD(8l?0iXZ<>m8e%=3IIEjq<^-u z7wFxy%!+?}$9+v5R$Uanqkv|s#-R#!l+VO8B{-sZO75@wRoz~00N5{&kOq^rci5EXQ3?lkJ7r!XRrqAS{KJ4K~Gil?W3Wrz;HTEC_J zYEecXUDxPp9GB!^pVkwC?NS{d(IZx`tSl9N!5_#iM5{Bu}G|cw?)JO*7{J+oy8;%Icc&j^_=L=ghZzt=CL1n^1g@>R{S<$9DXpH)KVm zmJ}s$;3nsYIGtX2IiE>XAa;em=_t*adMZC*W)OPI-3;(-wv^#77fqO|KRLbIZ!qx~ zze1R0EK1v6cE=mIh5LdrP!9@+JMJ;wd`IhyX!hK}vnIv^`Nl!Tw&BZp1>TcAO1C-d zheHbtEmRN=iDY%hW1>npfsE8ZZ@7baCdfiUt>qq67cdGLQuPI9EAYT#P_(`;+OFBb zJYMt_AMT!5AC4u9HQl*NX8D+3@2Xe-;?Dv{nZj+f0&4c%H>=K*enbLZ+N^M9sQg(+*bH#!!>YbojW|Vvk)wczTpvrDY3fK zQt}s6&$8hJw&Ul1@7C_(>WQESKSh|y*pE!U%VHfkYP_0zN~g2VSt+ukKe_-?-9fD| zPr6X75Oc=HGEFgfgPeX)%ke|5PMg5AIYwg%4?C((F`+T&iKkgMuMra?4~~zF z?C;U{;(RE!g7Si=3V0l}E#Km}b$L1NA3!mH>;1@#ffwiD*<1{<0^U|`7MTU8NRXGs z-n9dyy;mO&aU;$8`)9It#NebxdzePo-y>NKlzJ55RwW_SI8F$2#WRn}w#pd9Ag6DB>?FvFvVNZk)#_=s9 zyEjg0C?B!0unhChFLzsJ0N}x}JcE&N!zM1qKhLT@g2#}ats=~qNYb@a(QJcr%>~E} zlD1-`H$v-^=JiFasgNkb!Xf|;|F=HnqG;AE7|&uY_L`Mkjhz2FVjAerHG_wTl)y38 zvt753!d^X3bOzT|kS<%8HhF zOH17C?brWx_?gwI&rhJgSh1w8VCI65U*R<0|JdJ9KwYMdn=_f8Kr)(E{7L>8vOx+ef5(=#> z4Jrb*MX}z~ubF$oDngQX7@x82*yA1J@F36!BTX0ihBicD`Cxo-IvpjVJF^>gXDW;5 zOZKx(8ZExPX^_KFVG1rIh}IIdMq~3`cRtEbATTiz{kO_cB4y6+zvEJ(Q{k zZoe|e<1Ue)mJGUFO4#0+eQD!p3r{CGN^_tjx)$~OQfP36VGXIv)I`&K@I_IqAt=Z= zNxJ853xM$p2ib`NQ$o_ejzPu>P3(gGIN{E@D7c~jmCz%p)5~aZjSH-oey>j(GZFv6 zRLqz#g(+oO z?}NZs47~Y7rY|8UrBi3c#H3mQzS{A=`|a%1IZvjA)DI5{;~hFT4+{v(6cPzXF&-Ke z;i9lOQt^uVJf&NZzKBllmS-+;i)O#92@9|0{KfyE%svd3M?$V4FWt{ z!4x)uv*yLa@PCN_*Wv87Bmfa>8m>27C|5CUSr<1fh6qZ>Kyc>3=JZ73byOgg*JJa% zTPYF{sm4&y^g&p2FzJ1dt2~{^U8^2u9wXMZU_ySu*j9;cTNBT3JtzccmaAN~)mA1c z!gSdf*#bTWW1@gQ>2LeDACV1{pq?k&$GcMk6Gkq2OKUVUu#A2ubs>^SEIqZ}>4qcX zTb&!WjF=S<<%Xl+b7}5a@oMcBI;qT@$*?%LR`pBg1Afp~_TLy`?YnOx0TEkO^%@&< zFaODFr~fvuuB#b#yHf$1Su?5Br=Km$TC1IK96+A0$L5jJr;-dmzx)GfbM*a^CN<2? z!7)TKy%L6O0>v2*#YX@;!OuR`jU!HQuH-)a z9qzA;|EWyS9J{d}ah56e0$4Virw{LBxF+7H+iA^rozz-uFt-~2*P@-R@NK&Ic#6_$ z`m2TPl3H$clcdWFZ2qjsaXD8`Q=O=VTm0oI0`Stiw!NYaTGXk-5H>${M4NBRoh4~U zTsGz3VX=)({loR*zurd{$2m5pAVR6THfY4O7-f#4k5H%O%T>?|0%!jEic}^!b7eSF zMsm)7`!gxH)y>1Anjz0t4RLU>IHHzem}!GWiB%Y#{iK;OKV{7%Zn0@hQCE}bY(6x$ zVOam{FWf0fti^#cVfU$~amkqXm_&&%Q7A^^D~cn?PhnXz6F05_B6Ib$z3J28y>HaA z5J_cXm@uoVAe10xFFrg1(dF(FPfB_045{|JC)|B!YZj}C>rN4Y$Y(0If?+q#GS6z9 z_|Ch@Rs=P5&0!ArvoJ%N+~5qQsW}{r2~iSJ?f5hM%z4>637}51`5}INe?lp7 z?DP~N%C}vi=hufnTzs*g&Lx~8sV-3ngRv6aubm)TebVtqzuwop%eWfIkv01WZu8Zp z|F|qM&6yzTQ({KZf3v!u9#ixNPUdmUHGh7pCkknr;7_50Y)a}=X4Gvc%lkicon=s4 z&*Sx5N`c~5iWYZwiWYY%?(Xg`#oY<+THM_=P>Q>|26yMV-{1fBGm|@c!(3M^eb+>bJtXgM-eU*9E->=6gHAVb(u)XEO1 z;me|yt3x&JjtN3R5d_G`g^sMX2iinUR2)+;z#;5&cK3;4sIpPvDz z1OUnv0`zoO*JRoh#zZNKhs#*s-+P=MCbmfnj55r;J%5))-D-PWd3|eJ1WT9uA_gUg zjIgJHSXG{~9k8``vY90@B2f3VJLgXAH+wi<=pGy%mPlFUvpJFtnRwcCy% z1Yu}+PK@)5AvEp+rMao;>4w(Uh@*89B{P7f9B9wYQyeZF#!e{QFyvwwNl(~P?{ODm zi4RJv7}1{EmQgihL1Qxg5v9)Xt-cF>RoYkrB|34;w7sbLEjaiyP~q`k&w!(2!pMkZ zNJxmfX8YuZ_jqvfItT=skUX!q$3I%z*f@aoyXH*d;B3jfhDlhD`V6FhqNpD)df|1; z|3(``J9%Ug+PEtd_`i) zcZcAS^$sQoeIEsLoS#3?&3UhTI)RYPa)olP8PkDqz(t~Jmj6(v9UN85*fiGA|(`W z@;JfB0_RPw&M%*6)s@|XbR8F^5d(=nl#H~BWSq-+dAq4n+LT@|Ig`7np-cGhNS~d; z0_eMwAKdhOgRia;rcCPBoB@dSv!YeRL}-G)5%V$oEaCa#Uc(BgS#%b1Na|iUVxQ?R ze9AyR=zUxJe|jMp%zLBMB-`cG-A2Owvo44_N;XrVC1Tl2r)Nzl-;CS*5BzXw@N7|-#GYP;5=SALh1W}3DkV-Asc%ui)NO}Pmo^6UZcQ!n#Y$^~mew?ou z@zUZiXD5`U8e6_h5}O91C+00RnrgNJJJivw6<1rQqNM~EmsMazdVLm?xL5?BMF2?J z)wRo9k@j2?mLL?>TbDmW6w}Y{Xs=a|TFFmV7;!2~9g&PWrWnSzp3)gU?W^ zAS8UTz^NdRbjdEiHm;WKoZEVOad&&QEbkxp&FYkjRs+i_7Q=g!dmkOODJH8|wRN;F z=PO%Xa^}!oCGw2EoV?+-KeJcK>^~frr?heyvT!Dt7&xYmiUvMzVYYfB>u_2$5U9G+ zRDdkKI~vCNqxtSE={tTJ9^)355}v-voO>p5j`~AxC~q zNiY93Y}(>s8$r0CW9VU&aoI8fA`TSLtE0w@1mFqy3kO5(3pZ5w?+`SuOEF4a2O{7G zp*cG1a&-jFO8P_0k($Fzir4)iRkL$^4#L^uaZT}GP>Y_vwy|+;Y#Qg5a!TI|)$`dM zN7FT9jH120ofk0i!lHZ!5aVJcGg}!OFBGh-tQB;Om+P^9!)ZcM)!G?5%O}{6&f7;n zdwu_$eG^{5W;9)*QdwH5W=U>*7@|F>)rlQ*jK21WhLA?wz@xQta;Y0#d+%^|`*e5- ztCeuBZha7u=9H$S?37oXsBiN~ThL=*<=_}vT*L;f_5Zab{|ECwU2ZJ-3rQKb=fI1z zuDE(?>Agh*fj6z{u{Ja%fXX_Q%bboMV}z^N+wEpM-bOT=v=|rBtP~3+S9IDQ;la(_ zugaaj(sFXlfcpf15C;}Ofcxa-neg4!Z)OY7g|{W_+Sg1?TtK020(;tZtZ_G%=|oli z5{gE|ln>Dsft*92;TM#7KHRFFs8S+E(PB;GAZeMzB%Y67wHz%Z;%h2Ze?$5eglaTV z-0eoeAgGnZ6Lz1~pI1N@HWc)`xSXtlw-joH;hT$Mh}6&86;-U|Fm>#_qS6#=Q6N`nDfau{uvN>v3uj=PlZ`iVJqg{xYk9S25x7;@ zTU9V)Po@^iM$=R{t2$rf&ikee`?XQv(1H5GNAK|idxmm4atvcDa|JQ#V%L=o5@62v ztvRn>Zc=xc;dNlKZpS+PiS{Z8C)7PEQA;f4kB#O<{izVuSTzH?&xGz;Lj~d|B#hGeVp1p)JA;13={tY=HK%rL{UL{CDRbORRx@VTuuwuBj z3SutQi62CPnpctejfzl1NzHc;n3UBOnUZI*rD@jGbYcVEq24@ogm{IyE6En~dxibN z={iFU^n;IRNj=(?Q;S(ZQd5>3_5|N>5(L;cS!xWKWoSbtk+kJCW=yzTa+o$NB?CDr z5tFSx0{VLl%E1*7tc+`dPXC>Z&`y8Ln>h-RDOAbThUQcd{1e&uTU$oKjf^px<~}Tm zI#tTbTl0#lS;xE7lsDf>W>Hs1jsM{`Mw+2mn-MSypxpb&=uq9`auOX%zD#@NVCeeI zusQJMNYx9^-TlZ}sAZFEJp$52*ds;Mg`f~8@a1ADheus4*lLI3v1PhHhNutG*#cv{ z5Ne#lW=J-)HZ_)*uj9k^CAOK_P&lPx>I3$)XkjE7i^mWK6MK@WsbSPnJa6k{WB&Yc zjYd^{m)g$V702kxe|Sml3GoFsM81w@WP-Jk%Z?SkU?mmjGEh$V`ZAz)ct*jyegFs;t(0hG3glAV%U*Eqb|KEb%(W9&fi2SGFj3h>B z!7AEQ{x1Ha=nx5f+J$dJt^Ax-6&)%dI#XiO7H-4%$k62V^wF#YY0oF9jn}me(kxX) zb0B)tin&Hvu~OJ;10nDH#-IzNZ!U~)z27ZEfu|QijUV8|xohoIy%S_QPigJt7Ld&y zXA1QI8y(Vg@?Wl$m?O;TFYrs44#GsdcpJ7m9t_M##{)~TvDcy94~(o8ki39u+9tts;(o}sT2|=Iz>TnJ=PQN}D8rJAuTevF2PZgJwELb&>NQWr+cGlw* zee*O@Kaf@uGfz_*SU_lWBPCq(?2I|^SR=xvClkO!y1mMgv-(#^o~*3&{2?o5x+1zu znz}va1kE6_>prFv=*EHt4ve*;)O<5*e{encI%8ca~`B|i>%F?t&+cU{QCVXW0bz^A-b?c zdW?AI&adXVcQJ5*!84mpPI5s&<#Lsx)uH=e*o8|FxA<={B4?;PV)o7w?6v$Om}HB% z8X5FtL2lcG=nUg+n57m$j$$Kh?ly`^b`w4%GV}Ddp;z#Hr0$hfkcJ0=?!)Xra$fk2ER|)~Wz^y*@SKdC|tjPUI7FpAnmE zq720+(@YdsC{;*A&0CfevseNyv+TAPY`C6h^F4r`alGS=btehW{#U20ln{Ue1mh9^ zC|kYkF@B#yRzfeqV2)T~>-t`>`s;1!m5Dud!ou4?dk19LlVKDf{h4Z@@46BX+m#>= zS}e0ihUg!cDz6|*h7nuFruEb|kZI$bkiQP5b?Wf&NFFsC8ZN?c#?Jv~cmX&?T))># zjJMrRua_~fd`oA`plNGW644Vu-+SOdq>YiCO-R)*2}*Z>a=77sOS0i{O=G8Yyqwn?kzSZlVA;q95`SWL^fifvD@zj+V1ddvSU=YC=p%`hncS3i2O5wDcRpQ-nW{czP-Xm z7c4CwxEr8uNmCeo5#l6}K^jVg68^n$eRcPV5S#+^KlLwI7&)|8RCpLk3W7s(POy35 z#FiZCR3RT-DD~+|9g6;hDy15&bphB`9ZSqn<)o7~sI>{;!k-kMPr$d5KBQl1uZm3wc7i185K8Dx9 zm&crg5moFA(D!BIH+{bXN7QR5s5nU99P=3XkcNX<62(bNa8c@tHEemEtl&9oTceUTrJ6ga^K)Al9 zaEw48|2V6QH}7E~0-oMXOaSlv-TFP7TIKCeIitxbyx~iki?d(E#Qt!V)I0Vot0S*i zw4i4kPV*2pMXqGY90CB#mMlJd`^;}kD^W1U;zp4r;Q@gd@R?-u2^VssRB*0Fo=!D7 z^v6pPbot2Z$hF^Pq-io1QD7gyM0c_Ns`B@iBRo}-AxngnUW$Ny5$7f%5<;`-(2W5s z)YQu)%d!P&vbO#$GD%vq9bp_|`Nw}CYg-Ys#KOj6mQ`$k0ys4c<53sKk2vdXyx{VK z5|6!A%zC5GbfdZ2R*r5KfjR3M+iUHkK%G`Fa75hwYCAD5=?gFC zyNvkcP*;gk{go9lq>h?lBI;_-%Bh1xKuEWtwu1s7btIQYac}jo!Ua^RG;1>`(SZKT z@c`l-FcAYX_<}iUzlPMu!fmWU=gg!tgN9LUY{`q@NTm)3ix8mjgt;DEbgqCP52G{_ zo(97_^K&7Iv8lK;Dwl?$p(OTX?E;6Z!S;UC!jg;c4J`5>L8n{DUGPseo@Tr*-pr4j zZ!(lV6h_De$=o#;zU=NmGvdng4oq^9+BwhROp|~Y$(5k`*{K^Er|zC-Yn}uL3_%+` zd$S2jt1sI^1QNTWi&h{cV$kSOIk{D7fFk`TW^{7)cm7ddL4^ER zGTp+KLBIZ#oD-PgA{UgqMJOYFPl}6r(`LWx*m}ms##eA22ECxIUM6N?TjWT|l?MV` z?r?G3x`f2!my@AQ^JDnm6q->YNP2uFeCOUR!A0|~rA>U6r(xLbu*rri1!(Bo#dwo< zn8Zzd;irLk2?m5ndgc#8l&CPt2I#RG5@Y(!q!(y9qv9f-1(2_==ZsUAqB#ZUQ;1=R zX5`K~=jg+Ed3jZYDc@m6skZ&iXyp=JigJ9LpWQE%BD7S9CQ2El&88*j9 zUL?I82T@7&PJY`s_AR~FLkwW-p+YApuZk={7Xducf9#Z-XW9Re!ydag93Bb=f9s^} z_nnYcXJ$}$N&wQ)D4Gi*%>g>zyWVC$C;FO8_-^B+FzEzE&wu*=p*Liz~ajPVtuWLA0|? zPlB8g8EP^}tkL9JzjI)%2x*@cl~sQNWsEt7b(UJj@*CUhPvtF*F1JQ`?Q=`K>x+EiqsK! zBxdJ>AkpP(E)SsY{Y8>Lx)5*7YElgr)^h0{e$&&X>U!_D%Sw2;Yurm&Xt4GBcW{g} zG33DC34{z{eJ>Gw9Uy$;6w9A{JiThy;4eNE5yi25PKn^|{$Y02`6GHG8@WXTPBIJw z*!v)X!FR-oa!NK!h=T^a6xr1@-) z)bxmb#ucgVW=q=qsaHO-)(^o#A8|A@fs>+qZ-eu*=Ok`eN0nlW?I3O+5@n~b)sG}@ zFbgi1e6by$-v_@&`#FDP*jN5+75)1e*a4q1W8G6z$g1*Cl)L0aZ8`DNwlot?{%3MN zV)o1yFaKW=l$)9N`ggUDDtNLZa?mhmDYnVi?&5H9f{rKLdj95{z z>&vhd-q&zQIrZsbJBGxnLdeNx^M?h5q|v}{f0H~uRvj35eXJtX_xc!9&EIa&nay|! z6;6n}300c#Y|BpW*LqSji#njRbjFHKAb zDphoPcb*|Ng}ZXt8^s&R{s^>gaO?IY*7ci?ozXChO#BjA1n|>4Fv^<9^lIdT!$WKw zcpp*z^3yK>=F+pD)7RRWs)-05(J`=nQVHYxfS4#%K7A*VAr12zm&6AO3W9@ECL zCtXnNH_q1Xw$XjvtmGa2L)_xLmoeCQgf{U@9EUQg|E#!EdKq{R-WTToO{5}R^u9(M zL*hIPE;t9<|F91zLLWH=TE~FoUfoQgs@>J)GC${p9GU>bqX%ghjVb$3}}B9=$%_~2&wx*C+z21vphr3iyjSt04^_}1`z;T{&$-bX3$M5 zw;@Y{^6RA3+|Epbu;VuV#?$G!>y8B1?u^#`^q-5`(U?zxBk$ z5#JY*_hsps9K&uO`p(1 za#7Vv15}<2RRCh$9q`kUnw%4I`6No?<9xTm??A85QSgH0L+TGwuyDXTMczvW1Fj?? znf_T&hA<-;jS(Ky6&3)SSZf+UPGYBlgNkCZsgWwNEb|E!v=PMQqUdE(zKE}awB!W{ z8)sej4{Hcr9eR8(j*}^ZeuqGCt7FUyWy$-S=Hmc49G*FvRU^8VD+g$MielL)-6gl z*~pVQR@NH}uubrCD{6^9HVM<4QIfQTO4R-xDhjJ!R*g5ub+ESQmlRVems4?yri zM-4-E2Ci_xLEDiTWZR6oAdwnSo74-q+ua?65ilNEXaHS@= zM9_+{d0sX_h;dK7E}WZZP8s`Y=$Wrfsr70+-L=41(X);4Bpiry3G6fK^ zTgD1*`I=~5v>A!A!l-H0CtC4eptI2@R7RX^oXPT+eJ_m@bJnzqbgHkGTY7gQz%e%V z4z3Y+*BG-a9DvFAaz7QoN_|WR3Z{Orrjuqw7~zOdsNZh4YWZy=$#-o3%(RAdHTE4S z&nVYuaT_ov$vT*_P3)ziRZW!Z*QV@Kwg9!%aV^|De;ZkiwE2Nbwcszh-ArTO7$z?6 zKtO$7EGwt3K3fh&VweForL^Z%F0L)p1@!J(>N!)LBNZ?jQ89MuKKhgJ zD9%uO!C1)(u7El?@b%B_Fj!qe#KnlRx7K81+r9YN4{F|iBZimNjZ1n{<49X{@E1Fk z<`Q#FPDa0&k|q>e;0k}qG8EF#NxD7$`AA&nEe0oKL0(O1k_%~b#t*ezA-0q3AkuIB zesQGRvr{HdTt!xw05jl%9SgfU{H)DjeweOUHmnh_#TAu`yim{4&0fk@yt)qpG|vQk-`44yo(Nk-X7P*qel-SO zc;!Y^MmQ!OGQxKC&oQm2SMJolznh&41k!6wU-Mp!Xxn-@b$+hAo0Y^Yz)mFVh9J9Z~;AO{b4Pd<9@x%$45tP9shjd z&g(Iguywx5=Wa>GMQ%=1n0Sqoq3J$mldE!U$;so!2cNBcA(?TK>p?h}{L zwivgnL{eKkqvX)ip7q9kdaOSc4SjBB0kNS}PO|AmP=b+;)wu)u6&A&7Tw(T` zPB_6$MhqUgzTortzv%1Ta0`ByEvlR@_b5vFaTZq1Ar>T6w~P)ir)V{Sw*HKT3PJ8Lx=d1QSw1ow9{mp0^eA+#ZuPM!D4OpD41N-bW*>4k zf5P?0t-%@_mp|K=;M79|9gGuhnMU{4Qg6rks{Z_zG5o@?!cCfd?y)>gBg642ab+xZ z314Oerb&(RAT>%>t{yMl+fCB%U*N-;Po+-~+ZIwHR2^aFhY#;Jk|Ki2*0vugX%oOh zh{cX>uZ)32hee1{1z`y{e^QBmC|Qh+?G|J-Md!nT9X!cW`L{EwSR+U}hz!dKuKGc7 z_f19|+|0JX;dww>M^jT;IQ-9{-zj3+T^t1VUHOC$ws7CXk-C^jAEq0&kA8{vVu?tZ zDLawUuc$OQ8^1}64}8Vp$mhi~NcJ>GUk51TpXPOD*u4HCz83mk>?EN7T5z#w)C9sB z*YqHvXVdVm+$urz4D|lqw%+^+qo4>VGG>DWbK~~%hQ=<8`W+fqK3Ug{uKX>CSM}K% zIhd!40=zP^aoZp(6oHq0Rx+w%xO$^35CxJK#!*V|{CZDK6|-d;iWC zb$Iu_=7$n)+yi2WP5dA!@C6^zEjG=h5Hcd9MC)?W!u|O|UG-u{+@_AAqZ#LFS#vn1 zoOL{_44aqLfF=8DM;q?Nv1LzFc^njclYZA9`l2j_oPx98ITe`8Pj9SXx|m%fL-}&% zpifK7bfY90qXwpq9Zgu*`<}y1Ib0$>2;P? z-o~EIz|*j)s9j~zZFVc|*3Kv#&!~Nw^_7Un{tW)=N|+Xd1YRgE#Ux%%h93!+X`|4_ z1z1`TEA2MVjD>2tx(1DsJyAL~p>^HsmM*+n?faI_wf(CrWViX|ctO}C5*7kzcvf@b zKx79@3>@UYi#bqgABvN9Svht+ zMs8XGN0k?kMKnfM6^Hf!#d=Q`P%^x8V_+?v_3*8cK2@GgNy7Wv;9|0H6lL~HFoVsh zCxD0gj|ul5@{MstsV>8d+_~0_$W2*Z1~sivt3EOT%&WQ^gSSx1+uT^l zd6@TO!(w$;>oBm*-rw$ZuM&fgU#}Af?XfPtAVa4W&J&Oc2Qqz*;@WgcM}17P?ls?E z_Yt{5V^7ehDMY03QK znPXW2<=Qx4i~XChS`-fYn^?mhy`3ffVrCI*7!DKz7GD?J(e++hD&-Jj9hSH2(C#;R zO9($BZ=||&oF;A(|+@zasCCFol;i;CF30ITSFYdMR3`J3CS0(fgRC7lx zq}-de2URIe%L^9Is9f8^Z=0)UJA$|Ljcj4Bolp^>I=MdwIFw79K(HL?uakM?n&E~v_&IQ!)9+ts@8S&61y7(Y( z-}ziRK5m;zgtDiwbeUQFzaSd_hR1QCVXq9d;>n3p^3NKiVxK*U=8l|+JocZ?1wrQb z`_~>xSvyj)?Gl+((%%U|QOnFYP)($vY8+(U4&<8~+ODl_80ci^rsU$I*l&Mv0=s?3 zw?N&nR{j8%C#ca7|zh--Y+@T9rR;+ndN zPsvcS-6ZkM^?T<+VYMgrY=7Lv*y*ZIKtwul-de8{QAjcUJu6*1aS&%>irMH%M^M($I#zm zCLGQlcyl~X$n-ppF^!7w_kGA%&x1qxBR;wCDil0#>sw))yVS^e+1D_{Hf6o-LuFA> zL>MAc-+J-IjPPIuM@^cJmrkAe3RBGJECi8M$Km zFEJ=RKeF}Pm(e}$v2ysmMhRM7!sED!!@4LVyB-IB5Qk5Sfb0JaCn;<45SZ~1s@GbM zcty+F#^$mQfsu)YW1Nuj%iDvT1ge#lP%0V2qV$qyI^&fhWc$B&5ulIKS*BILYb4{~u`gSM|a#DIh{C#E{Z|>Uz;;Ba^29AJpPi z^(3h$kVilk6nHCD^s9@RQ8vwcr*Ke@$LKmbkz+zGJrghf-#7BFTHOjOc@&iyI$5@E z?RfQ&!ItGs)V&bD1y6iO&g-d7 z2c);}bgu)PuLNZN@fN75QrZeHWaiT@YuO#T@8F*pY_P$*1*c)}Msx8uIqg6OOFaxVtJBPGWn#lB4XHHxU zwETo701(rH`9I@Hkp7k{(^R-)x#2R~=m`|~R+Kb@rdfG`6C;5UiG|pt8#5LhsXZE? zz+xqmxbi2n)?~N{U;*%OS?R5p;#%TezzulcqE89QX`ujo2~ycv4G*P89Nq5ATX-b| zUETxR%96DjRthVcl!t70))^w5RTUzJUR}s6}$VZ531+-Y|(MT%)VPBXkN&QYw_7!94j$2>z_Ij16@5e5tV3PAyl zSri){2i%QfiID{fOsd*d)G_kWmqnCvOv$La2_jgm1?0Y$O5Yx0`JcIRyiT~x%)m@2 zLIG#~|GyE_(2A-syzG5@r1UQqM0CiK&H4~5uVZXk7-S-qzx${dKAkAlC7f(?`o$1W zWVz|yNdEMVU|sL}TlkhGJ>j>f?q-zt0dFXL@YZ6j5mysy%O6yN$Vw2G}Gr2phv!1-IB*eHA-sO;!@rX<1o=%~u_*XjiW$ zvfj@lww$w)xUPJIVrq#wQxzh4*dy@xq$BebGO9dKCVR|uu<_6fNuzt;M@bQzKG#iqjaT&6tKaQmu@2+VUR z%|=+u6fRe*m)8$}CHQOlCmb*>JpPCk_$MhIe5qEgeOBy2)|{94o=NqkW_f*`A|gNA zWU-D;w(sRPQHa7S`FLQE-G!Sbr-bYCN=Q61u9L7$BW5^WRMTt*FTd&4OT%EwK7E(x zz&3=E&i68DY~p+U!%4-6(hdcyN0#e@tMDZy!p9A*X=>LLEoKM-{ovevk~xgBX&P^NWi-e3-VnEe!DGH-JE?TCLg*%Qc#QxOG>bX z28P?*ozAp+%~szZIn4UWu!kwjVKH$tg3N!?{G3+ISt-CYwA*-#YH|3JUHCWsXP&jx zj9cRC*e^0)1ib$32TMwBw)5fC`woi?{Tne}L?Vl<#P&yK$owm9dW9}dQO_XRhn;Yp zUZx4$_-cU;RO}GIk1`dGw7wi>4r|{;GmOKGmyt0nvZ0Q{;f9Om=g6Z^|AVl0x*qD> zF2wsfWa9lLeu8jal?~Kiy7Xm8>Ia__^X~xiKf5f@t;|VDY}^dzjAvInS}U_WI+|+? z=Nq~^1$fP+Ha5A1f^m4H9QY)(^2R2OxIqzU@KL$Jd-DoHn+IaQiUTnNEXA%CQ^O-j zT(t;*@9JnR_Egy8@atN;F!mtuvXxcxm8a9U_sE_fusE$c9BjgaZ*6olIZ8d4rEUHw zk9uE*V2;-=Zf7A_Rcf?JcjZ84h;)oncVIr zxFcE%F~QAD{k~W2vnBa9YUdr&{WeJXgPBi=+02M6d7bL^=yZj7be5F|MW}kTE0B8^ z`Mq_A(bVs*dyK!=pSXxDWsKzR@#AT*iMd7rCBSru!j;*-!F+o~r6tJR6My;`mK5^@ zKw~DZYgv9&agsxHLyOtByddC+w zv#u{1$Gu;|iCoIePDkFs){|_@mIu1j!ng`a+AmRkDqbxnaXtECRZ2^d$mHi>Q-8d#ll zRV;+5Vl9cr-E}q@+6>}A_($Ab=_aw#wz*shzgaUi{I4kPAr_eV)HRYSf{7k)rYj?$ zIcJhzCR}OMtgz8fU@^wsi*$S0c0f#g3i_#8Yi;?JClGp zl@T{c(sPZrJ^g~dp`q&1SdJxO%7Vm37?oO(XL5e6l1$yUy?n%uXVMn=h_4YcG*ElY zhw%E$@6LNqW3l4mGgOtG9g43v>xB%!CZ~zXKJ;+!6~dRA0SS^`Yx6I)>@=~=^8~&((ZUA1sR=Xi4G__9&DnR^pL(N zwMY!OIBH@)YO;nEm@gfkx&N!ky6sl&_0kR@7FcDo2;&8tQn(+DIA!v%NS1O3G}QZu z7aK_0%Y-LT6fY2&+bY?ynLGOwDjFL{owr(ZHkKhu5%)91r{$ro&@K4bTDxhiS!}}) zlfCSliUH)82{I?f!S(`Sh{c zx1s0B76H^jXjRETxTvr;r~`* zBzv~M@Rh9GG*&B|Oe_*n&aCZyOfodfrNIbrutrwjeP)E2YNkUMLr?=Mj2xWv`<7ZA zVq}!5jI%`0We?BSLqSmKBJzJ5?MJla*a}Y8!2k~)D3HrPKtWd#Bb#~T`ZI>3SW~d+k-^N zZP92h*<}K4gHX9*glp9Y9PnlBQ7c?N+gT5EbXN{us|el9L`259Lpl|S(XO9RKVPHlvfO1yIL-x9{>7MVD+wbVyM2FpON!}rQ=7O{=dZNP$|%gl5yzF;1^Iu? zTf#Vf4s6Zuwf@DjedsmC9E98-P zMLwa(y|3Cg>s})xiis#`fWSOuesk7|sVG&~ir?z}ab_Gf&!>@|noEV<-FJBaE1kDV zbL_i=+=QT~Uy+|0%~xInr;<_~mG5}$TpIBMRTOKV!JDh8gGXh8>(ie`fVV`*7^Hn} zTvMKy$>$LCZJcngbmm${HPYD0gQCo2d_3li8f-R%U9O5s6_pz;8f7DeNbQy2{-Lko zS8>c|IMTr}&0(HV+4Z*0F)KIR4j;3~a)_)e&qpcT0aX|rXSCX`yILs5sPKI3pu2GV z5HFoj;T;9q<&Bopr&ZmR{oB#z76%A2p-y){gm7*Dl?M_OzJEKnM_)x(wd0nNS%zKF zAk?;qpOwCOxub;Xgc5^+6Y<*aq|Ym~@M&{u{<|1+dnJQfO7p|io~UkT-EeGG<>gbf zi*9$<-}01{2YrJ;P!@&F0)+3`{y~ynM^evzD5t;$C?61uTt=+~oe7~lxi}MuOw>A0^JMC{T5UY#oDyp|zQ0hi2 z+BM->khdH4HfSzxVS$vv#*#eHiIl<*641ZlL)ZU)c8Mt9wz~Zi)@v;H3`59>!^>fM zwOTk>Vn)}+WOfPih&(8v17_>YGV{nw?cUrIb3HZA*&sFizPdH}Ggyd{W64v%;`iyct0RrbqmE5e zzWv6zxH|fpqMlexdcE4YXdL^-wLl~Ckr@ma5#lkiK)^VRdeW#|EJY0bXZ!1pf;`29 zrGWKu@l`@*sXY(3mR8cABJ7YU9^W+RIIKufp!G55x$f}`W;;LTGe%Dtg8F<9aajKoBQ)P za`o!!-guQXoeT+WESfVajmVeqYUsuoc~pe5VEo{dPJr`pq5106vtBvB!T{kx0v1e{ z-ow@mrd}mOux|55PaiWH{}Yr=&CF6pDw+)a8ZeldIsPV67N-$15^i&aG~g@V9z=Yy zJMTfd|EcAsQ$=TUnI+x)cos^ly>_?s>xZUkT6E7=V=#BM&c<#fp4F=MO5P4qp&9>f zG0MF6??R*(UbuW?G4_bY>na0tfo_Fc*gC#}2-q4kq}Zl1VZqbODPB6~?rNVitw4nD zI5~0aIjER5>+`;ef|j=2M9}tCG#-}9Oc^trN^!$Kf~WU2XMmMO^Li_Ae(?5OlnkyBS}t5 zz0A#SHTV%dAYPMKN6jj#c$b6TG(o$^q2b-B0&`5^sUzJmTk_K& z?u*m4v9JY>%}Rnmlw%6cW9A8&^nVAKwbjilu{XTzxJc<^6frvrG)LA3uf}dQ@={g2 z6GmQM8Jd+L7U@Y+MLsO1)PkhQ)Tt4fHpD@P@o&AiD8d3;kc~HeX2ihO)*o^?{9~6h zW@&sLF{G5^EFQaFbCgBF(iaLq4J0*m7pl_r53ZD`j5U4&N~T}PLGUY(=wJ1!~j zcd{vdZ&hGB-P?!yB0voc?r6txFj2$k&#FSMfKSM!p?51Oxt~y${Z6>BcO;p}_;00y z%Z6ExrACIpp2J<{=ndujPG|D%_)*+-Qqy@e^u5+B>yTK84xYwXux! zDCA;GZdfaWShq0zJQyty43>G3xU{Qwhi3lG&$zOd>c&CRF_8ZQu^4k#MsOV}o~ov$ zN}9I^pt5&CHXzit^Q4y9ay)OdK_h8pWTZDel`E%)D;z+Did2yhu+3DaxgCE7@-t~d z!5O!y@Af>CZ@r%d#XOWnLnkKiIy6QvHgxE(&efb+YI`F1);rpIx}SCy9vA$cUFKh27=gbaQyytXtkNbFPt!)jWsG?rN1<0)yb82U?{#YnyA0+El7YgQ&r(Jyl zxP~Nlzu!OHe22f^fUGBYaXyZ0|9-w`obZ7dk$2}98+&~?ONqBJG78jtztuaKZ=}tn zg%M`0Yy5QD{q*h)K6sWUwzCd;K*Oh%j`Bxwp|xnAQ!$nmtbmUGVErX0o%sh(zCce( z6KUb>(jU6fX+aVLiIMrG5mzl8%{;P1+8V9$U*``UTo%B6eZ(9+F41s7NxO2;yQMPR z4Ep2`6vD4GRoY8oi~!sGWTxc!muIby8ybijHnjM{up^>Uw=ga=4h}VRvLB8R7|?y{ zl%y;a8|P!v{(Tv$f9fZoqWAkc5b@K3U@4<9HGp2-nc&P_!!mx*bjF0Enm$@qp!m!( z`oD9a`8C_-NCRwO&}H-VF>S0lcS}ND1L;v9VlZ{^H_r6sIP}0{KsUnzEdGH+i74!I z6~s_u-kIN@41%-;87$U& z-{l-I#pln^E)XVQ4+85y59xN3hEJy@-WIfl`xaJ>iVCh|7#@Q2m|QT>@cgSW=8q9T z2{QL>9U(2CrVXd}S68EQ-#*-b`-HEAN)Z@#lfGtC0;{Je)wagqi|u4EySe&jM2dF5 z;1n+CXDK$LRD&;ufb-0(MeB^w`56BI^H_}d92!2g6j^{)bgEku=J~v}t577W=B8tljny5UwhaCr{#)^LW>EMAFG# zq;$O@>3Uz17Z(?Mp6@8rA3j`SU`CKPx#DSeIb@7j`g;%c;--G)?kNu4)?a(lTq_aA zUK<;}GfT0xxAF<4QZ=>?l;&Kh0fE!dBCm~}2;z&0Im8Yg{~dHo04?`xlwK@#Sy^U< zY^m`!4>551F7@LpYR+O!tY#C6bXcv8&ZafgAM_q?2$=F-l3rZ6A1<4b2qj9IhnR*ptI= z9X~reqqGP^33!fQ2P%DhcQ}0mD*;xJ@^)UK+lop@Bic1=GfA2FME!D%g&TL28M)G9 zC-mQFXPQ|&7Xts&p-o!8N-x&|eLC94xw20@UGq<$vOWmn!eoDA(o4b?X)zF?Ou&@- z>I#un#$--LdoG*~z;5$@OWmjd1NTE!okUe@E4M%<5o%d$NVZ`mhErv6qF$;ZY;Uv) zIg%b4wa*ES19gp!*95Ev*_DzPy-R>1@XEf_^5htEmb>2V{PhvLL+kT>!opB!MtMX57&jsZHo@Tnn?^-rosDGr+v} zo(k1yElQ%wotabJJL<^JItsCO;WS)PaqjrQ8C5G2cufj(P#zFzL{UPUz$^~?&$d~6 zKAj&yOV+mfw5CS)oVsUq-TLZo%!OfXY4cPIQgOm|bSf!^NikSV5VsS>1&q*AUporhyx?&%fJj*^Hf(lwx;`08 zoU*fJKm1jadPJQf*g8arf8yi;j4zhW~?<_96TdtjNgqKk_gM1 zy;{al8u+yj__!n%sA9s6fcey2u!T|O=GEcd)d9a=!|M7f)a?-WGXwu4;(si9ptrZv zP4MZgo-R^|Wf5g36~bvpn*TdoDqGuh`1$@Vql*%Gy|Q}mCnX4&U$3871iI<~cfdgG z=P}n%!}+b_X$oXwGcjOib05Dhi(=%QJ>k1G8WkOV+a(%U*sM8j)qT&mLeOWMpIjOd(Lx zs;S-0)$*3j=k$@pHb4lu%Qb|Llt(B^`yF)y$tguin00MV1YdOM=N!6}z{{^DQ2yOY zZ)W={iFXI?l$UR)^p5-FE2|`iwae_S+ELV+OolGdnY;iM0cdmAz=5#09eNXexJ%s@ z9o&X>rofaS_moj&@sU5&jeMzSx}>UnXg}+ZTWnzZ(iOVxO9u26IY62i!TD~Kq9SFW zf#bO8?+4Iu0QpoEFshmv zMCJ!5 zAVgz!%aUz|KJe1bp70wO2 zKM@l?QPsx*MmYwdpS#3jrbL3NQ3p;`>P<6Kj*o!f^XIEek)10u>bf0Yo4&Gi+CRDs zR*oZW&r9TbZ{g=n_WqqShC*~!v?-Ci8S(1HW0yItm^2vNoLMcrhBb32@YM#88n^w( zA!%K_0ruC|LsfE(@jLh7gC~)_vOmU#a$QfCu3cZ7a(W*Q0UC^q zx4}VPXR93;| zt5i0;r!8TmK{~s6&2>9g9`<2W1wNr#&Gn~>Cu1X{KCUf<2t2R-MuGI@l^6Ltr*_# zAbX62!*r57rI2AqOtpl&Txh&3JZ|Y2V7)Nnn0tAXb zskz4Lys9uiFxm_YD+xbDulH(j5JjY?@rQvuiwZ;9zmg8%yUobJK0G?g$QB&he`hW} zjAh~W4*t83SH~x{MvzZhNA(DiRdnPQUno#2{~(oG6oy0D)+vXs; z#ml25YbgF2FhISfn37A6)+UY-_sz=;LJE|n5Xo2kMX=J*iG1^^KA;h|`o6%*F{O}^ zZ1d64tX=8*AvLMym1{HHi;0ldWLR57#jCxvBAhUFR6`dPqu%qRfwraH_`G8^PD=|S zs0ZJo1v{E~4cxHqR4C@SFaA289KuxV>g}(dGUO0lN<|ed0sWQVYu0R3^gCc=t`f_) zNTkUihw1Yi`v;W+Ae+TV)L88ps%v-YKMyJ8bYDAfwdzaV0x9)7FJrXEt zh6**NA1s@sC{G$1)(@qGsQncmNk}v)Lr2x(o}7$fRUWyWoKV|rZS9*_zz0m5^m{Gl zF=4$(sSoij8X84txr9!hj!9P?%{_#j&RO=|-6L5`X=TmrrJ!#Go9#cafU~*4%*q{$ zFF2V%DgluN2b4h`?w_z$aJJ2_5(u&2w%O}EZQlDjUfcQ7zX1p>XM$m76eyoKrt48| zjsqWzF&0;+$N(&+fK)me`i>`&>b2Y)wIKAc(3BQ6beItSB27AFzzUZl4Q#Y?1-<~T z`9cCR00{I3D$UBuHaQOlB2crrokQ<1860kEhdthJhFhPPjkEdPb5%+doOcJHWAV8A z*4Oa>bO=zMaYYTxeg7&0fR3c+=N9$KQyDr}M!sh_zd-LkN4!`1kw((}nEgL8p< z&{QGY$RSKiZ2)F3!NC6=I)G0Sr8s%*V=BzjyqW76a7pdI>&VUX@Y?7Xol6yM)FQtWA+!;za7V{Vr3ev{ zMCx6B<^ohCu1Kb$?OXW~>vH1eGvsenyn$1b>40MkCwDjJA^xl#8PJ;%g1!|Z?aR?i zq5byoS>G>-%Y3eq*G7gju|^fPxH#6$Ra@HE_urmHKr1Py@INW+pCV{Rj$rhZ_E8yd zlr5RdU$8 zLikEkKz|;dI(m_q+JA{-G~?Fh7+K|DX)Du}_!EQ8;#Q;~6Xjg3X38DUF z13Rf^L_ixRnV&u>%Ezw1W1(cw*0@% zIi=aY0?8X+FT2*$n*JBIR(4LFQAhR>r~k7+_e0RrOHP{LzPsM%PYnUxp1UWVYeaXt z|9Odqy2cm{x)qFBYiJ-9ISPEVGP3xe;%)go0VEH(J}QDuN2*6oRp^rqJ8!fx^vJ>8 zfDwWSmZ=-#Vc_w!(S(4~9z}d)`gKSc9^`&zo$^i{{406lT4gGG=h+0E6d{9V^?lZy zU~fb;B)ZaEuwQa2vy_Y$H=^q4gQf1FpgHb@u=5A^axS9CsepU+to^o>%&V_6UWo>< z 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 z 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 z zPgQKf!F30)&Vj+f^0KlIpb~c8&ScBlwX6NW3zFN1>`saG*$P=K?TFD;Er|jJbYLdN zD>iPcvFB0q$SMaH`SBh9zTSf0{};Aazab?CiwECq@)`XbDxB@cDgH=$UQIu@{Rp85w4M zm2z~$bBfI4CL3yD^}=}cD2_Rgx)7XX!c_+Xt2|0lV65BI1j<*PO&&+yP#Q-SE z>z3C$+|LEfSz}}4e&;w|l-~jqfLB2hIyN%c?^OOju^*J4gIM&(pl|+z#&%Fzfa>$6 zJLHc=j0$Gscr{`Svs4GVJP`?tv@ z2vcK+;pq16v%9NJ^(eUDd0wCL(tZt`PD z1*+sVdk*f@ewK;hj!Go}L(+0!AdM8{zCQLWgZ!zVpgU!2YYXE{m{!@?)IZ@o0HZ4R zr`6b?d={#c_!l&DgAu<8Wa+G(cUE>QtMCK;rz}_ zN?3^o9e$z}cLM62nfBmQqrWW`EHMjqe@h_wq&{!>dG*KK^`=*%)1 z5eP*9F_Z3IfKA?{9+ZL%`N^3VeC1bJa&-M_DSfkzOZ7)eyDg@P!EeV;gJumXiu-X= z>X(}C7vGmO9k^320gGSWKw;i^5J4L6taY`fw)Q{p^MQCVK!kei z*KK(0RZr!s+MpDeV(d5~JAA$q1nR&G57VM>i+0A7M?og34!YJPUMbg#B8*w0>*8!y zpuBzUw;PWDtaEwD@2mX4di7PbFh9W|K=n!ogXPuY7r=qN>_rhEtHVpx$XCjcd3Qvg z@%F9gP+Uv7EhZR7EeDo4_@U|3%MKU6+6`5<+{6kRUF4Y6$e@J`Nq_00stz}an=+scRHEu4u z{40lgI*$eveRjb19$30?%VLvtZvm9tkuF{~t$FbMM{Hy=(MBz7Q9#+E1bzmxtk6?p z={>QZ{cGy*BQCH}1Jc(6mb38E^1AR9=VI@BhXk&6uIk2-5&N;|E`TuMXLC2r8fRgR zbZ&Y;(+U`ZKo^0l7z>*jGpXdaAHxL)N5U-E+n2G~CqmU9DmYDWom*ff zP;0PvJ3*+CRbsli2}JL=vxYM=PCOl1a$ZfZ#=eVRO~&)_`rR%CtK0462b;Lc%_dBg zj#JI78eH&Ld7=wi_@p{GjZN6ceY!n1&JYXPz)#PrhfF1~QP75~KZw%(nhW_P(dypq ztDlbM$n<#sWGrr!3-0JHFF|O2GRpvp`6pfXG}pA85^Za8frP%_#?#V_WMk`sw#ioV z(NMw?=QZd3x=03A{rtmio!^8+WM+nj4(&~OjIGEteurP)9NuE3=f0)&JUvvruAo<= z&PLm*X4(na@B~LXxOr7%Y2UteB>gR>4T^YyfDs5+)oudVR9WzW+T;mvT!763h^zo9 zkUW9rs#3L6pVkrrMU3y94cVq@4W9@A=eFoKOs$C61-e)Jv<)M<#lDAMzUkUkcVg>| z5ln)Acc10do{%0~tX||}sL7U~-<(*DZ2hEB=nY$(%3OQ-k|`?f-D~%QMOk~hHJfUj zb*cn26Ce&}N;Gl;0mhu?^5c%IOz+^jCbL+F)tpINHiMpyu37b-dfO?_9idci@Wyss zv9XMl)+jTr$0XT1^ZnAW^T_?ZJu`5=0`1t))nn}9@bCk*lo!SVe|Azt+jq+WF7v?74){Vt9 z+oMSp>9yyZ&`%|uB|lSC?gzyj{m!_)N=Y*5cCYu!gg@+G(fWI24Q{o~#{2|YOS+(- z+`QFfR4{;Qhc61v{`39;kyy-R#WRt5F2v4odG-af*^zDt=rRNy*PhK`F0VTE*uR0E zEDBAnHhaK^7X33@58l`xodyk$;R-Eg9D6#_E=2@KXNN^#^=tWqUAJK`?bTNX1&Tv zAq^LH%bQn3w7)I3YOHO;xs#Lhmj2DLM@;s4=}kEjZVfsVrLwVS)C3)}{|kwuj4N{AzI*e5lgg-`xvWctB{@|D=&03sk{?2>45`RK57a ztef!8PKh9FGRknfZp0qfo?EJifFgTx4wx`)yrswTfHFqhfa6a5^kE_)9#%~*eqjQ` z6OZlBE(LVRni~)vV{)R;O*^_#p*-!2e>u2j<}|_Jd&cLTV@i0)u^;iF>R=x{ z)iu(kZE7r|5spZ8cGKDjlJL%Gm4BWKVV?vT^=@|pJT1YIwu_kRLcm}@wTdzkD58RT zL2*1ShEKibu!*Wr@9;%&HODlGlR~;2e-0fbpHz%qu|<4zN5TvK8&Qm%g-ajlH%y^| z_jdf+Snef?c86*BcdBpvlbrK%_)FS+SEl^XTB*_7-sYyS8qmnTbHx zgw9h8cc?RL2aLTZ*8%U5v9NO7c}E@-lNEtowWr0dsIt32K)OSFe1!ScBw@;Otjy9J!G1 z)J8VMvrWUnGiKXG(fVl8U4m}tXNNsxh?r`#rU&@%%kbt)G;kFvdEm*k$XarxK2Ax( zS3iW8lYckMvHv*^K+>5J9F?-g;10FP3dJvi^1C&pTMN^KxxpBxw(gpM8S^68M)qTlcX*7#(H@9C5 zy4PuGLJj8cDm|gOQy9Qr#q8O>R(re(n|-p8msrd8u6=*NTW-N{3ne?CZb?xbv2!Do zSFgbRt`Q#vVriwrE>=INKs4_U$a}Yl2~G5zNNtd7o`anu*jw_aQ8`tn;q-Fmt5g8bouAF3Q?tg*E4P`d9nK$pp4t%8ezJSu>R~v(oDTUa zhQv~HR``^;;iOPOA3Br#X`vptN2uu6en55bC;p3N+3lG@r+y95M-BK4v@fasn1a4Y zb8vUGY`E5tBJ0P(Nz}Ig-h5?CFuK_Mk6-FqU4gukSuK_)Lxw{9L%F7@n;m#UV-kiw zvW*s2D;KMSg>&^%$NmUFw~o6J8a<<{f6t%n&%KjpA%kW=DW;BBi7D}_9J~~badtL~ zoO}2W6gPoNZ6M{61Egm9#w$zaKUS=Rs_>Cg7EtR7gl-J++v1;r=ui}eVOHW%cFPbC z@~j{Gi@?|iy5b-Cv;4A{$S3cKBNT6TtRI}GT&=dOb3`|w;$JZGpj_QHvnO;PIsBz= ziZ^%in%P>h#_W@POT-HYyf<4vZ@K4S){58U3X9AomB9XF(^{qQBZ(N<1H{3-^>z8G7@ z2%$!k3hDGu=Mwg{~b92+|79i1JQUfG@zvJ4VvGl>!(FOU|UdX`>SzCCd zz_qrGjbzgE9bdL@(KBG7uR)OQ#aeyO!m9aS=Pi5kfa?!i=XOvQ|1T8zolcn`2BMNJ zrD9V~6P<>cy(DK1P4<2*m|wo#Byxl_3Q`@io01=v%h?LTpVGG8;~KGr^G2B?)ow|bPgYPdI>#$W$GKCBN9Fr3{U;a&M_ie}YJ}xW^Vt^OJa8YQ2 zFZVrKu<}&fHj$`UGG^BtrJDO;e-!-NC~-e^K+3yfUDS-Cd~~($_xfgbdw5T&nYN!a zmSa=qRIu*I{m~`okBdDDTNQu8enoz6Pn08^*oO5CkjqYJ-+uE}S&)2n2WDI)_xJFR zSq0Lu;-4v-wvgX-XK6Ay9-nIES(wu&_PuxEy)Z8KAhOcc*51~XNEh-A)6n-!PSn4B zWwGdorcqT3l@q4&)%q`N;(4(357FEqr`%R2@VRctxS$LC9ajl;=8%(TQX?cI%T{Fp zdNeDrsX6E+N@hC-+YAEHE6}pW$>w5aLcvR%g}W($Zo=H7ENqhdMRXBOXr7LX4eIdnAp>9xw>pwYP_f#plFK_KOV{9Mi)E2x-HC^iXDCs zyNYUpx76t0ua(T-T^e@(edwjS;kfONvKM*E3Kvw$Z5pi_i|3Xrm|bdM%~;*q&|k@> z3T#Izl()^KsVkmZK7x{@c4d^xJ>H}{1Qav;1E7@*oCLZoQtX~k@~}+UCIfzWZ3zO> zAi9$iNYO&=K~rU4Vx>EYB<7|NR5PzGnqw*Ug}&t%YAIx4>dXJP!A)3?AP`?{C$_3n zZRLV(do`)a;#($AmZr0oB51-*`4n^s_BBz#+aiP`MjFWYr7br?LHRSe@99UM#f=$@ zjJ3^??ElF0?5bkkgLTTfI;{9?A`%@CcEblp-%6V1;(-1n+JVUr%y11aN=rq)EtJS% z^cI~!9!-xdL36xI4i9DQ!fzB1wfNjjv)gEzb}tq2RH+X-Dso;=OaGcVCQ{&^8?krZ z_&nG|{7K@^00i5ee;YdHT0yHj6BNDc}wZfd1yp7x%5yx^22=^S9hZ5BM44#-aQ=yKi0Aj zJ_HQnLB`?Hal3%q6#4#4L$I3Rzh}*LzEE?L^#qK$eh=RTe7Hb znOv2$e8W>;db779e+trG%P)0~vcDKUDSI!%^>OmUMh!SH72go-{Z_wTr2azEwrx`o zT%79_(()E3bjVL$cy@l65Xamq(Dph66u`*P*;i|WhCl!ly*P%vS)}x|0g|lMl1g2rird8EBm`F9V(vXT$)*$ zQT#$eMHW;kANgiL5irML!Hd6RV`Fvr;1lQ*U&i0kBTBSOxj@>=W%QJ_L=}biR}9Qi zyepRCT;-PiY$o1f-&2_^%8tOSHs_9)b#3&>MnrKjOueL6OCHiL2Jqhg~mTmkv#l;8{0;7``vbtCs>g@eyP)h`9wD=U*BUj3Fw0 z&;H>rEMjYi0L{Y_DT1eYg|VMkf^lV`P{jBKvrsI!mpQ3t#U=@` z(V^BQ=^lB8Ugp455Kg2youJxLXk&#OTln2*<2jGVh8~dV9N}}1RMW|PmlPV@X)4Jw z4@3#UmYfQOrJD19G@ivcAye>w9RdMIc@<(W(x|7X z0IOk4|5_4ghXukSCT8YW;ho7B_k+{7`Nw&nc;L zwbD~o6A<~QiJxv#P_Y|1Ik-cYX%kQ^Y-9d${6H?gali&2KQj&}Sb6czE6)yJTn;YY z@`H7}>2{6vB<;ZGt30ikRJZRdW%R9j3wOlL);vrIT@0jxh}4{^Y!tg3ATfJ?YxBW^ zAiIe_H=;~XpQcs(V6W?R@T*<$3%+2HbZv0ZTEgn^Fu>?m989HUXwp++e_H2xUY5Yo z&&@Z}NBgN)a(*~AvZNtq3MA5fj;J|;pI?ykirSZk9ZMO7MF2isF>Y$&dfSB$<8LxA zXKbuw_6RDqYX+80{F5PTwyCL5%^zCz&p(_laPZliAZ9@@2JkENn6m#Wl>?qPJG*^w z29j+DH6d~|Bqcwpi<>s#ier3V zS8FST4v~FLPfs7p$MJ0(k=VpZ0-9|xuR$*tPH|DJ=YT13>)4$AIVDfqz5w^$+SxJj z1Y;WjO2D}pBP?EnH{V=}F#}?uVgoaSmO|6WsS4!Oe%jG(t>GMjB!qCYRzyGK_shVU z6hk^I$FfBu|I&8pK5fpdGA%Y_k2?Bfd%Az#nM_FqI=Xbvn7CUVnY?hgJe9WAYI2O; zL*FG;5Xpm8Y}_%$*#qqWGY8o~w7R%t!&t0h`ZbV10O`IDea>8n&w#PqB;}jR>9K5_ zuII6U?k+YLtmkfgPRkJcyUX|fRyVcTmu~0_6^rjqG^h056H=7;vUrDLSHx?$(3|6k zN@ySSvcbJ`CcfeHR`TO`CmU^1;9AJ662&-&N2o3FKVna2$}PvLCb*bK_KwtKC8tOy zwk0C2L~S@2Qk#(5^xLdxhP8jR`E1e9NaE)=CRNjW#7gYet%%>g@HPMKDC->#P_y5z z_}@ld^=UMT3S~40o|Yg(kb!gt@M#S3lo`YBD67`dd>`uy!xgZ%yyY8+_xQibXMT*?V8Gu(_2J|c2CwKrA2i1S0y@UngyD$6e!I*uuMN>HVE8BlBWW=dP zyBPD0gDa*ALwq1OW9CFH1Di($E7O~rNzAcLc9NfH^wSNx&GIFyY|mI#k-pm2$sSklNaFoIjt;}Lys8(Tv8w5IGT`c;7}hiaN_`&aE>0ulr}v0L@4 zqP(Vg#Q)HGI+)m5b_aM#H=U;wstILgR!b_y55p^;_yeX-UgmIdE^!GalsZnVwXagR z!eptUKe;erk^-u>qBJ>nZFD&Gbk}(MSSL1^o5Lv^a54kS9EfDpOvl)x))dN{ zbQj6xO>brjq6uV)U~>=14~q}?2bCt>m|#BQmiNj{AOhC2rAM!aU*|p3OI$iOPm+=n zCSnZjq|XZZ#q%Hll8kpN}$noPj7G|>*# zLdU>}k)~FkFR|*t$Hy;ojd16XC?`hWVg_t>4hrR%`nq=tU8Se=cgY@X?cozp=&>>) z^=1v22lLd6j$Zg5zIcpzRxf&{N2TT*InWf=NtqBiecC~NfuS`wvJw5arbht+|1)>+ z>?vUJJ%Pc)_8s-!qmupWb5fYaUgW}sc}y=1jDvCK7kg{b+yuy<6bKO2sG>6Pt(C8V zNmN9N+PgzVVJ*5)rbT%C>y6%eL^cnWjqx1GFey{X|^w{Y(jbSZNMby3D30IRbOhcxoN zVb_Y!F~)R1Yk$#AiiyR8o;wLUDypI}5Z*hPN6irQ{Q7V#{c{c@{Z-vN&8wtAPJ84+ z(fKzS!KY0xE`wR`7@Dt(nqPX!-aL8~Y12rOSDY;&1-rI$JI| zvBZM^*H@`9;m_1LzXf1R$BJVP?a;<>hJ~G7BpW(da#X8|0wAU(I~Be+ZUDVByNiyi z$c~EaG$z|~=1a>{Wiq7Vsd9gDaj^Y1QFe*XTDD_!s?RV}b?xjEu7Lk7DNZg@cSX7F zSEuT(^m%^XGJ7mvxF8b?^9lUT?Kh}SckRO=ba%cyH=CckmW_At!oVX9I0B^J__|0a zu+E+ty1BtPqS->Xff%wfa_dugCneZx41r;AVL_i%uh!JZ=VwgJD@iFS{>uOC#I-Gl zjH>)2+Qd%=TXIr`r3E)*RX`hTICh}(#?a9a|G{4n&Gq*$wu&sMiL}#7(8K7fu#Guc zspRgc_%mX;WXT`7PghtM?FxMoNWMiF>jwXZqvMR5@@o4GNB{Il!-d&xOqs~3j@XL8 zOBI^eE04dg%@&bLq>vJmLlod9OqchHgBWs>M%E2UmalN$Py|*ou#N*8v74>(fE#J| zmi!uaOhA%kOF0y$L}CH=ySZwzVU?sK$PiU5nX1?=+_dIqlAAAmajiL?-IXo+(&D1Vx-e2WK9iq)G0mIfy1Yab!4L}o{7Gs#(5JJn=b2Tp>;Sa-@qgi0%WQyRa9}k(6?K@ML z(MT+QI?Z%)64jk_k)0NuLIHCT>0#8bM#+Og)49T}ysgZ3*nkE&n#t z|Dc%59#5~2UF2X1rDopotutPRO0i7WEAXhZ+-y(d=O$EWr`k({-H57OGUwthK(2cc z(+EcUNK!EVeulokcG7$(cRR2lyf|XX!o+mg{^mX!_pPGtdSDqjN*agJ>{RvazPiSl zA_7K4LSh19T>vK;+4cT!?)=IT4+!Wmd$_;zs8=oDQZkOII0n8WVA{&21%X=<=>{0WB~Sp;l{uLJiT@gNs)c(mV1?Ju7r&R05(K~wtA zd46}N;Y_66#p;NKbW-@0oLteGvz<>ENT6EqJ}}#vECx{*j0yMmelWZFKG65h?`y(@ zpPgB>pL~*`_#2oEAKjw|@QdT6FIY|pXbrIW@pzvzUCg_`dGwCB&$|-T%)9AWJjO)S zEc)?-u;10C)-Q98-DHm+X%ATB0@Fmu$vb3vz+zf^GQ`i{&CSIn3Etu}w(MY20|>Vc z%oY+OtxWeCpuj+jc3!P?upYaB`^@2A!BEwZY!1O48w>#rWGU{`j_8$qMFx>d91Q&X zNd^XW&-<=UH4dc31*&`zf4y=@8Nr1@)n!giKL7j5FT|%*(#v;o12}BcpmKuG%zLUz z;bGaI-b#n=@KFn<=8#ng+PKG7*fDIaIsLPFQo_PElBgVNf~dd`P$p`%2m^ZYUo>$- ze@!PB=`{HYy31AB67ix=>O!40zn0MGYN$V{_44~QNFS1m-!R7H7GdqZD_e!YL+^Q z%wp6r1+@cyjo7LbPgtRa*BO&Zg`np#J^x{|I6cPMI+OKlJOK!$Yw{Y6DzUBWL<))% z!kT5|bX2Tm)`??7D^{x|dYw5OvT#f`hp-Mio3Ctqc2_bCA5VRurt9q@fs+9afYTVY z=S+=0iwUIIjE;>LveD;I;h?A4a!CPxX(%G?XCx4mfQ%cyk7}QS}1f3|O{- z?JtMGd@)`Yq#m%afxd0vwFg5u@auykmT$BTY~Q>K#^oSFC3n(NxmdN9t+KUx(4KD9SsX&pp$fg1q|l1b;ko0~tUeDx0`lmPUa z&-XX)23P=q<9~f)BN_ZizyZ+PD+7cM$w^6u-~tH^MWv;s1wU*q2-&KQo+&Ne!3M6? zZV(NgoSM3FdvyXR8yqrRu>h})%dj6w8BKwj}xW%H#1Pf&{Ln* z+3a}jeUu6Qu6p$jqw4az@+(iG?)Q>3nTD9jh*0ucb}>8qGBRu zRtru-$?V=54fK#u9~RYg^RhR3A|ANCqV#k;yP zguB?Dp&+V1Y+J1Yss2l5g#>C+@~SPl{QP28YR)&Z%rcoY$wg>+g=~vemdN-V6Q6S> zr5WRDBeQXrPR>sRmEh7$4+BCc8dZ9^{GOi58nFFP;0s6t+$qw*&-;(H7a=fE53D_#L1d5 z6`yhX#_rT#?s16*nSPRa?p$b~^wu~B_nm;0XBx?SZRmOeBLKn2cT_Yc+w=BrOgV^- z>zK+tzJs2J-MOb*s*@BXfaD8Ce9PLG>AMUbN z^YB5?jOy6RENG&jJiG16ybKfi#f@xJWs;D--lU1&Se^5ee0@9iKBzPD=ivU&l);`( zwKl|=q`w#{r@`^DZi{=Dn7MVBI4b0_K#v?NLYF}kTSDi9!jGD(`c-2Z(?SMK%MY#Y z`7lvys0g;KE(!a)=@@sOzc!W3ML5gGu)b9#9){Sx89q6Ecg=)eH|`4=4IGu#sfL#Y zoU_Hsqv$Rb$@lP_;bC1pHjN)l?`d*jvX-{N*s^9X5+mqwQjeL86R3Ak&(HJGk6Uvj z{6|3s*G3V4i?*$delptCf_Tdajff84?oexcI|#Dm+ud_4(V1eT_PI*qQqn)Ynm#}K zvGDshwZvi`nXW9mlU`z##YqpHkNNM;<$!n@!h@$<7(ro7{&(^{HsP_xI`fVpY%V{r zsmMMo{@&3VhCx-5nYL8JMxgvW9-A!t%@2N$pgPXm|Q)l7t54fVR-p~vEfhh7Ih#d>{*fJ9ezJ|}_+ zzwo`Uh){-sW1c5Xl6R-L7DiC|2L*uK%$RR-Q0+3@r0%uAcuh(-Bk|;67oo) zT?;Q;MS)HMzhon(moetfOXgs;6D>}zbAR4CKj$fgI$;Gns^FtngPMu`0%SjbHC>V- z`A-ngFy8O$^#yL_8AXg)F1*YM!@hUmTZ*`f4h^GNZ-I+3EJH1ZXiN9nOI#Tm){9UM z9?q>(>q4F-STE(Mt|dQA?s2_ASaZZ_^Yb&u^#RcbB)Ndze>&XEY!Egc&BR61wDwPW zUP~L_*D<{O`=I5lFjjR za%$G)nz&yb{w5&HOG%_O$b0SO9&KoD9^wVZoV`D?-qG@sv;h&xwp`wwOb9S7@Hthm zp&z;I2XFnD#C6#WLDdIC1F&#TqU#f8jVpwhb`(|uUNnEoii;$%xRP%isbTV@n(Itu z_{dtrw{v(q;8ynY&D9HBll zy4dQmlQ8R0f&FN>tTUFTpnh?qw@>UcJ+;CXr^+`J@ekzNt<@j|_-QO<@qD>s{`*Yz z1wUhP<#>9sr`lkx7DMD^FyFzRvf6qPjHVM7CXJuc=W5u*l3apH+>}E+OVKLw7bR|{ z8A`9RqnU_)&o4eb>faDTSoAR8v-s|X0GUYS40a{+S#$CGe#g6-4HsWYg4YhffABH# zbwauzifI02U~-}CXv&|L<~w2IiQ2JVzjWEkZ2zUFR6wtu%7vH;^eU@J)V+wshihj= zpD{V5ODMHbt`!~_zJzGnL~;Cw-STjx9e$vI`Vz{iSzg5)W6{3n%VkhMxEAm#@p604 zN#aseuM#M>`THN`jCAj5G8TMYM{RIs?vEIu>)KojTR5_4^rAk(hCK9FTk|X#qt8s# z+Xt!PqFC(BUWJ2WKOkeAC}(&M8a>Ky|9z4RqIUW5b7E45o|4pZ1F2Gg}@PAw4@GY_3Yb-P_qaDt{yfW|dy^i$8Gty9?Fy)h9wVnn!#U z>mL>gO}jHiWCP+3FAcv$MZKRXIey8HoiEDvaP1M!e@#Y?*#0+tI2azQLF{|mh70!1 zs)MkVa;V(QFITwV?Jl~3=e8>g%ecqsNH-xVY2)tQ1cmD<*I!S``2(%lkOAFh3KUIU z-PL#v{Si#5#7MC;@3ZD?!K(vs;Dabe{!*!oFY)_$V^LB@k?+DlzzvIjqeu9uJ_dK# z>HX2xXIg7MNFH;4iTU8!%6IU*%cuiHu0j4w!>gpPZQuEP(=7kmRsR>dGR+eiZdQcM zHC8vx7gJnQllYa~>l+CCN@YX8+SX|91Cy;>INEy>8!6(HV*CinAGJg4dfK|CJjKKV z;EsmE3kO!y?l9X^^L^*AC(GsJq*NH8oN7XPxQ*3>fS0`#Wsk0kbJZL&r_Rkq+M(tF zfP=qt)}V7`ta*NxGT0;es=Qw?H~m%cs=IbftqE3*Q8tjCg;Z6c|JpA?&UBJfu`D=H zx1;C%z@eO8a`xY?G7d^^M78aUepqY85viIYH_FrKERzHCElWI=MDGdl=|+pxr$`Q3 zYAkZN&~7H!mhQyejuqwX%XnDbtJ(3V`7a6ZCE4zIA=TB zFdlc?pRKS?!lskRbi<-A8n0r&ir&f@)Q&wD(K0&LNBdtHak4%u@aMes{L|droW0VB zpJcVQzjbOy_!N&uh4GHecMw^2;AWll&uQ}h9+AsK^T#Bie%*E{?|vWKUwyFCwHyWU ze(Z}K_Kx%Qct|?q!_<6{?Mux4YeCQh|5hn|cRCT~;>4r<8+v_X%hex%^nyqDCd1W! zz1{&N7jMQvn${K^@aXz02A%Ecs#MRiO!{$l-5`Fbm@r-w zOFr9lM?63?Cts3@{B6242#SuM!=|4!qC_~{K+GLI zz&JZa12W?j=3s;hI&=K1$D8Vj0XcX3^=N-greL|JKjEn1%T)|-QX{ocP=oVKVI^~5 zHVk7+Sta2Xy;K(!R6aMckY$jy(%R;egY9^uH3|Zvc z&Q?0s!dpp{Fr5mnh6=75Z|FX$L#;*6+5#l8{Y&3I)W6irUTMMHLNTp9AV-3wWOo^- zWb}j~aNhNp>+?*CEQ79P{?PTPGXs%~4)8dey@!_a;Zo#;^)NQPZ|!%6lnw;{qz_S@ zEllbFo>cYx*!}ZWGR*VRy#>R}tko~{;owcFIcx0w{QO)K@XSubzuKj}pbhL_LJOBv zL1K>mAMY>Yc0%Atm9E2<4iEVrBW)q`f9;+1S5;5A#wig5q`OnPOH#U}n?p%=2uMnY zNH+*bN=U<@K|or%ySuyPj^B0vi2K83>2fLS$T@q@%$|AQ=lMK8aL*tkrm_innO5s@ zoM*%kp(7Vp7JEmW?TaYsjq2~6b#^nCF`<(c(A1}oY)q||A|Ez!Ekae~pUQx~Hu<%zfzlA(jxwUe=+I$zpC*Q!aFFw;j?GygX2>)Pg z><HF~#}GsO(EH`@YQ;se7iF?1^Rs_s9qHFVtKdJ> z#|B)l`c_yJL>@#bX1@0z^Up@;3upP77@x>$;+aQLm!w;zLo@m}hDxoGWzw%@8`i+k zu7aio%zMLJl-Xt3b=#mbzge}6hWSuY+GY5z_|`0q9SM#pVb@Z=3b?tcS)D^xAD z7p5(FBj&co=vaZoFg2M&Rkok1mqWExrs9rdM?D(xK~G%ON?triCOIdNhh)LLxq6xyg&NfGbTr0~(-;O5x`m7vK2iMVzg`Kc@LX=GcHI${4~ZMUUrm+dVhA)gmOp(!WB4jlVmPNA zQC>cs1!2p7@6HJ|)YT8aEH|Ip1f6QAoJwmi_rWKUSey>adE_L9`3 zSO7Ba^p9De{tJ{9Id-532i2V1NPd33mZP7h7v~!Y7qRtvEFf`v?&lXLgR-19GNdDH zexPkk%bJm#1%_6!)ej5%0y63Z!&U9uXW+K}_x z0>ZqoZ#+M~YYWpymA14p#Ky^6q=efA@1@UNK3kApwEgZQV(lmS$wReyLd(oUGot)b z=q;v`k8Yd;!;6<*cK=M_T+LI$tXIBN`SBSz=>yw`o7Dh=FQ0g*ptC4k6z>AS>}FKI z-GQSfBjg5dFUs7DO<`FVS-Pz*-Pc^*dqse0mo!humf~|Odw{=q&S+XUmjgC&0BJ;Z zc?dmWR93x1pv@jq#VE2jlkew=&i)Nldqr6I@sjojd3~&2zMV$P$-X9MG`XLR*sj;3k#34z_A$q=WGYMW$ zCsS-3I@so^0gK`!6j0{ib~xD3m`?C<~xX z&%!%unD8T2{QRqxT(&jTw$A!7LvfTDBPUn0Nv_jn=!R#%s2TA*W(ijnFc39aVD}w zNj!jik0^&dhH(L(Mi@9Uc3DR%AD`YnE)(3mZv48}VN%;J(nR0_rI~Gg?b&6qZZSX% z)WqDxtFtOsIV4T|97&@Fj7h9as45jRf(p$`J>#lNNwV)jUPSGT(oq5#L{YRxuXEAj z*Z44|G%Ll&^91YX=7+l7Dyawh;z+nMspUPY`fVY%&z8XVnwa_5qTs^vUa}?%NM@X0 zme__-Nc`qIcqBY;5FRtT@ithFRrnhY^jOVH$9^dOKt3>V71m_rH{+zRTnT2O!W+;1 z#+kd>-5%=J0ROGR)ug*v@;`5x^@PJe;fdn>>ns;~=i^P*QxTR}&P=X740G zWFDjCj?=qiF8eIpG_8-4QUqqr}=F47|ab(fzB8jw|dq>GAc+;s6_EjSi3%fK4W?c6~4R zSMO@>Lfqr@HQ3dGyC)!hB`9Qh`SeagPTi9nKRiM5n`x6~>+!B^pK zH|94=Vszs*)?Sc!D91aS?J!OZj3&w2xO(xxBpIrX_ws>s{WXbED9RD{%}QhB->l1$ z8xtHq3{DR1)hKm!oCA?&_ah+9F>*6D0o-v=gd!a$CjAWqG@wQ^Kg$}m|J-qj{WzF2 ztkP-GjI$SJ&%bFNX&vt6ll6knVU~_&Mb65sO0D-gbMd8#x9(&IG05VWG@!*khUwoyC zt(?UmfglmSFU~JnzD+ttA9@GeQeaQW2&A_uU_F5B%f4e9RZsUnWiaOTk?ud6pv3%r*M-2td zKkCNSggubg-k>&S_PG_)x>aWq6%`sm@d63op0tNj`U7L@`;)>WXD}5GHD{{bJ0oTRIi7EPaX(- zYs?v}1J1s-ByRxfZ>GRnw0j8q^jqjB|zAHKwZ`=_qgH|R{I zoUT0k;NS0JQ+CARtW~XEMWZrAZ6|+?Vgp^N&8fEltORSR5U3*l*+8nj-U9b6`B*{F zL{{|@C+@_w?_5RovgYzK_br@ZI`S=_e;mE<)TLPct=2a)T>G|>`F65AL&i%2c7^}o;zSt=%2w;;61+(iND?+m+3ad2 z1H6(w)#2W(X=aDja4;tHMfE;J_SqCTHm#`&beLUkc6ws61!ILrr8L|Vc!W!~7zn{) z{*sN~zRP70+GX&L*G5_3MY0AKMsK-{+d`+7jx%P^P^%Iko6fY7_4cDrX;F4;Uy`6p zPtQ|ObE(`i5CJth9iRLfj!+IhohEGriIiuVp4ZCHgydULaaq;-EN9nJRT*n20r@P^T<4t`jP#o56p zS$OWs+N&UX^~s8E)qBFJI*TIn#vt#FS3`8&LWOj$>-P41FDn^ zq#?&-A7y6)x1{@g)TNl49ls9;K|3-&*8m0Bm=!RI38=e*Az92~#w*a7@Gq?ttcQhV zHLk!22hD9cd5HhONkTS$P$Ec98Q!vtKtB z%tU8~!z}eh^)G_tXoa1ayD@&0##_?CXQe}RYQ6Yt77N!7q3F!;JL7p*fV|{B>huKetSEBSvsRQs~X4U!tlhXjAV#ff7zWOmt zVkT9*4b}E%ZI6CNAgHqQy_x^UTr&P^_xe}0{d|+8i{qlH@<`9|P6pakzoARrsl=9E zm5MeOwj*<}&u7ln{zwtaFM%Gf;?o4U3&JWt1e^To^=Kd=blFqTv3a+v(B?kB(~sOS zSJI`()N@dy1l}6(pF7Lwt|u+Ti@sgj4$%$x!1VbbqhJpg%PIm^Q?ju?_}GQpq_gql0$0#H!E;b>0J^#Azaq zc!G1Wh@I}LKnYtl%%B!eAIS{mc6Ydx#Vm>Nge6rT$4Hm!(qQuNH4pQk+6zMid^YI$ zwa|$A6jL*FsuhfMG516aLxJa%dlsoj){{YIVRmPOm&)0_zzEKpL7OrZFG~k_G`}|n zOgg`LV?#bdaB=pQx6=m^W7wKLm6r}X)AY~R+1u&2ve7npoo-l*nCRl2HB@gO`dCDD zkNE|vp^F$$-|4K)m)FuH$N_i34^gFpcp5B5y>I`tbFo(r72Z`>-&qA@=lag1@EbJP zzkrCm{aN}w9y#03UWG#m*ntzYa!e}ffN765aseWH8JX2}Q@10^0(i zWAy5z%f<>vRm|-(#%bde$0m{e)s#Kc5W*>tD*X)>P4%mzqDo$E)3U}Hx-G`XO>X7a zj{@n+EBU=LpJQ#m1fc(r-{SnxA1nBh1WVwsZZh#31M*=`>tp-hh4zuViyh!2{7MJs zaH38d4GUTU7u;=I6wgP`m+j6V-eRDD^MhRfk#7o8P{aV*iQsS0LVQ*>^xC6kETGb# zaQd!1R|@-Q8t~(TKP{ZBdwjkzI(xhrIk=*!znbHR+RXm2Wqg&n=*ITEf8!TpndKR3 zQ}73hx;R}r4WC+qWT=nSf7lxH`N8oy_D#0et5V(V4iin?KB#duou@6w=G0v*Uowql zDOrKVJEmBj7iUaTyNSU#?AIqDgw9pRqn~*r z4A{fUvzWheFCjRe_W?dliME`CF*-)L5kLwg5#cW^H(D(qTy6_P_uUOrG!Po;AnUmP zCNV#8?hoD6P*rhuLA77$BK27AxZI@ixR!@nlB#8ez`NeczoS=2Z|3 z#Q1aZ-$!u}#MO$X>9r1OA7L zGbE7W`?5`qK?!>+FeVo(Ho6y^=9>phV%B)IMz7dVbz)Gyph88Gz`a9*Z|52ma4bY&bCNrdDPyUE zhdB(|!hdNawAT362{XLlyiY4Xf?O{%Mxw&!W2BHWr50Tk*y1mp$fJn6U*p#hn#;9Eg zA2qtUZoNm^(|KQSaEkTY)Y)gH z*9`*Xc*&?|=`YB3?a$-NSQl#Wiir~`?LY>??e?9&qw?EJ)aZ4?4-x&u5#W#rG8-_A z7-)eTsI9FHEy0k17HF5i(Y0pocQrF8GlNtxY?cTrBsj(`PI!mNrMF2ME>Jw`2|?f) z&o``~zPiktRF|__a_+3Fn&(N+k)Pi?BB#k7`B(jrN@VF@3gQ)Q?LFutmS;)Yy)>ol zAaumPNWgF4Qs2qbXFeu(77$nzS3jQ@?~Cl_?rwwslj$9{pw2@^9ONCg7=3J8&I+oa zn^Ahn=c}0pJ`YN-&25C*y0^0^&FzEeidWiJFAO+(q3}0O_q&6K3e$JDykTK7OVLo5 zYQ|f1OCLX#Kd!sW51tQGv-LKTaHCS4s{@zKR zf00c!TuxOD|cJf|A9l}Ac0eewyCU+JK6w!Ah&1NBR`WJ+mUX5`B(?X zq}RC>Ut8Q!afR0&N|@TvpUEd;SD*5-VaOv9xpzX}^mu)~ZK-YZnA!^BOmnLPp@pEv z&d8r}S_4}3gLD`jKB52t5%r=sYkD+pbC0LT@*!yb=8GEn!)w0Y?Ir^GrDd2kf4<6Xd0XXcmRV11#~f`crD5{2NYSYSl-VkVQM@)zJ2+B6U(}2GU<$7P;KIhntAuVgy{gf1 zc1xwbM8{m^N)t0;=Lh^N7EOL|l;If?`qnggt}!qF@X)FFOAXuIjOAY|rEI3nt`#Wo zrT`@qU);yU`O`bhSwKPT*etlBV+1iL8fMOo#l!lIELJE|FygQr5KNX0F_H4{nRjYW z+1HaWoNFb&`MuF*A~X%#Siw0Y6Xo_?5Y~|e*{-*c&!k2LMXoszu;u`Kba?=G$AQ8u z|5lnyrA9Rg>N1n+D2ulrXz?A%a!7ln3knXd28X{bMU2X?f@GZy1;P~;>TZn!}p+RkgX>} zxIPp(d5rcCcUmx4Nj)alfCu=O9@m1CALS0ZL-Y4h)XKY+k1l89HRbX`FQ?)A=WhchfOl%;?QsNIQ-!GR=6M)G4~j`!?HcU;opT84Kt0C02e% zK5=!$ds8YWAB7ivFb41$bO|Ovs_9bPXN4ugzI}RT&->CVQ&VtrsOKTzG8dcm>&x76 zGDve$+N9hY>2-{Wt#JFyKDZ1*N^$mpY50hn1tTEtRVnBRA@$Q>S$=tH+3m zL2@K`W)y3@VxR>kSkh_{;GceH5P!~meLXAGbcMAi^(KIYN5iaat!p{!SkWmYolSTA zHFB9snPu+Vr2`gj8U-b>KU!My!ebqgkHR4{+;0y7b#rjU99=xM50?H$d$ z_z@YHUeTA%rt#ruZZx~Lzc*1Ir2LGfG;F>C1SzFN&HP_gI2=`~!JoLY;fgD|KmcS5 zJFyx%GdQ5s)Wr&WX2hI0d49T7%lE0FaFb!;u0jiTc|rB)|YEa*Zt zs^Iy2iX7|^`N37=3fcY3X=@B$y4=aqQrhs(l~fwH9AoDum^7W0;mSWV9D78*4P=|Z z&-{K|1L%}DfE#;g9eW`&L;jN|Ifx!X0Yd_)u&%e)`eKJnCC&$`72J(+MlJx75M+!| zf9%&(cvDL-WC3*UZ6_ceiE0Yky>k(3*bXe#BuRx`b5BF^mfL&lm`%K|3?U1P@oL( z4?rLxbS6|X`eIf7GP5D|uS7bn+Psb|Qhq*W(cWZwh23v?d%5C*+QvvF9w(6HI;bSK zs5WW;;TEOt~uztCi84J`27f)X$SZ25s&h34lLSvIEHO(tW~QUm;8 z=+5Ca6rftn)=+Kv8uqvBqmMLI zoYQ1&b#WGS`NSf=V?M{`@6_dX66TPD>{C|*9Y|VZ7ArZKeVqh_E^6f$FOI7&K=0cI zm1vFF7pj;GrUt$6?)X7gIMDa&*Iskw<^8l7FhGZyexP+^Z+mnH@oQ&uNi~hOPVhg^*YN+8Whm930^8U_j z!Pv_vFicvimor|P${`ClAK_{Fn+KRvA0;_5Dj;oTk`E>7zX|sHTQv#OJza6vl-We4 z!-Qeu8a(5Y;_$e@kV^C3JE^Dhh7C9QQw88Ow7$}!ZdFj~kG;nAwV^T|JEl!0r^z_! za3%pgOQ3{TGf6X3S%a4VMo!Pe;f0i`ly+S(sb-{sC&s}cIn~Pb`%EDV%?=~pxPn{! z%qZ}IP?^}W4FN8Ixh~LR06Sil&ex9uLw_zOHJPeg*{Y%!EJ#c31mKij5$DLu7TZSI zETtssF_vhdYhbByY3NP8E0SbgP+N*niTJ~x&Ri&*^e%2(Z>=`jp~%h-*SSV;Zf@UA z20Lk3;Qoo-#W3;k-O&PwK12=9FMD=CVC2XrE$MTUupOUl`hvtnED#Zr4i~dvqbCHw zIwHjCo5Fh~2uTg_$h}m1x?f1jf`Z#KZqFC&+q~v26?)d+RRq;@iq09xCjItoV-BA) z;M=ngIZfH|&B{O}c(1#OZ3Dw;s0v>#EX@1h#j1;bpTj7E(Jk-oC>PlX?Hw#YwJ*kk zF%QG&8kWpxEv5^qy1H@|wWfdh+HxOxm>Y|9$qMJj;=oR!R`xCN8;1!V^X^oqHTXPM z25nk3Uq{Z(p^D%L1Tl&>GaFv4YuTLzH)BkbgEm%;7dF7HY_QUfS8z0j3%2SRA$%ivye zxq;n(SB3lCUN(=SCSG3M6of)XGO6{ zz^%N&z*;LP;?XC4UO799{7Ody!Y1tBW_Z6O&6>UY@XkxOhUEy%(?us)C3fO~^m%QX zTdZP@Xky0r`B%7N<$E_ulDe{2eDv|N!dH&gkZM~3naySyL#jj@Mt!*tlT~*)RF#No zVa}6nSNHP>$eHG(^_rcAyoE^iotj)mc! z%lM#+=+soyk{8nOSJsg!_}QsvC#lgN$UsBY2Z!VpK*j4AAUN zstyHPAWG|lOfin!$2B}-g)dre$V$kTw`Q|#I7x2g__eW` zO4p8Px85a0cBDAO#Kq`p`1ZF8ZC?!S@!z!t;9h71GkBRUkhbgpbJu(sjHHoDAoa{} zr)cz;-IDkq-*et--Ct#ap6z?c3VnUE5BsR6a*|Dwy~CC*IDF|6^peac_V3@n$GF;3 z`uKjFQbJ8&9~AThMY{!`qQDHqR`&PzgHn@CxL&fb2vtMt$o6gZ>hJ3B4zv;I$D6l% zHThh+AW;(dHiJ!M8yggVR{eoA(vdg!%h@t@#4bB3&2Qd^tt2FypU`!(YfOFB)4Vo^ zbGMjEyaiauCpp|qXZy%^B?uK;g+%r@bj)^>8e7HHWz~)9)_?;Iw0)?w#@dF~wDlHt zBDGubzOykerW-=0KOG3al;Hi!3N2YeE18!s8*kgR!Pu28l_SAESJTiY+6$qp3uVth z;w;j&RvtwDV>$H9sBaWEl^Q+VLxNqbiNLqPU@bkbjwd^lYWz2=_cQ;-#iiu0HP|rp zXvlfKA)7cv*nv9sS8?EKRCjC>+g(L8OAMt{K@6iP0h ze(=KPmwXIohZZLAKR&>h{+(R~LWrq5|F4#3-3vk>=qsJ=?r8&d<=aPDi;H>QiNy>X zodpK%C>p?4I@x>iU4|zgIWN@7^{jb z(tZabepfS}gFau6{3QeoIIyIGWbq>{6kW(V^@qS1dv%d~?N)S0b`Vee*^_lO0~=%H za*?W)>79H(fK6px&ZSooDC#NBAT+!1J|QR7Exk%o=m>G?vu_>9n5fCwF1^q{uhUYL z&&Sx@sH80uXbMIFTu@HqFT9VltG$N@Kk>0>XA|OB6>XFpxaSmGfB5Om8%<4niE>SN zqNHpLg~U#T#FpvO!m@{`H&qdpmEFe|4+wM?Lg){~>?t%llHKqnbz#*QPp+ZYV(EN5h z{K5bN?L}{BEqwNp`PnR97^E>H!QYvOe*EAf$`w{b+lKk81+MUuSL^KVb?!lXhRT!? zLNu+jJxavyFi%_o`uWsLa$x|$> zmvUb+iAhcHG)CFU%WA^45aTf!`o9iV8OKt=yq%h95iomG+)uSVTe6>AK*J|qvg^0l ztYL9eJu;v$*1lU`sbaq`pS>5ld0t}9hmve=vd8O6E31;!o9M8MxIR#0HoWr65rW8| zy`RD=m#EcDzL4{slzvRIQ&ZqGLk?jen z2!8vm)Zd%W_sX6}jXzHJBO9RktAeOszn&gsd*1^04m_wl0JZu|+XK=2(Of*8#fM)( zkB@ABv5amz@n@^eQ2;pV@$bpAh|%+Ny@zF$^WqKZuXe7Q=9-&$oc^ui4-8{rn-W#F zjnH)o^eVJw6>Po}sip5|uMl1k5%&cJeMf&m#4Q#8BNfLRJ$6ltoR0TCy&)pAR*v}% z6kEUAZlA83HbsForqy!ixNA5%xmm$wPmPo3-Va8c>Tbj7ZL{D-=JR9BiSNr&?UA*r zxclFS-W#S1KFQ=Md$}RxMs=se<}le__cUPg`3yHrHkIe;FvI!RAU1w|x0>f6UNL0@ z_MtHTPW)d#2}PeGW^URqxbJj;TFF(%=McD6p=2HAP-FIR9}#pIq!95~8ss`XdkDnQ zk=Q{YJveAIyNy}lfWJ)1zi7YhW>e9`0ZubYqNq-8 zB@m}~`{eiAl;hS%grMoQ<^)EBWUMR=aOD8V4gzF`a*z|e(Vm_jfcHX>m{C=?tz;nv z@$0S^7o*&`P6E&9uTiPUD%et%M(vl>Si|m)rssa`&JrNrKTr;yH#Pm~gtM@;Jh(HS zXhOBHoN%BcS}0;o=&gjfa#+{LOOd8k;agkx*Jips5Rav)GlCx_jNtpgeY|>c2VDG-070$Q+|UrNR=mlo zQKJgs&2T$dCDDfq@E3eg_%TIE%sePrrx&88X)M@Vp;nBLJrcIIW&+%9UNG3*UVV1= zF#dW<9gjYU_-uxf%y_OlZ}1Ud1Kz=`IS~vFUhU+PwMbrGUx_7eLEoheZq4uS$Be;! zOJdwI#sFFLAmUXd8}zlX&OZIzaK(EaR@KmeRHxMi82;ip7$slel+;ZYu98^u;$&xF z%RATsJ)Ti(W}0FUmQUy2x9$D|E`lT#=I>n9zK-wB?UG{ttB*rbE4qw$OSt013d#rR zh+W*P2|lqtYP~A<1OkdRZAOx?NOE{yX0WYsFjOwz?R$cHEP4GylBl+|zo>AN!e*np zGMNYGaRgIi3O!v;d^+pKcitPPeSDgEe(jijPy4EL5Q7%^p_kCBzH_vBX!M~q`|oRL zxp@9)UJ?2$eZ+6w#8h#z`jys&^si96-j!)d zG#W@Z8X$h_9pB{y*c+H%!W6dY&~jG1NJ_lOvW4=J68nEQ^tc}$hH5xKQx&J4mE<)w zRs^6w;2AybYpc@ZoB|&{MVg|ivoq$us6}>y-{4vU7|YV$0Fh|Q$PNb|-^&H)+6@@% zoWSH3_VjSm#g1x~j<^7g>o)_K{iF9soSdA20Gj9lBOzhC1X4EQ`)z~h>h<|VMPt&^ z2t8IM8I#00)9i>p0i0z{B@K3)YvI$nd*ul{twgf)mQMGCCkaEqq_Mt?^~l(BA8%rr$rFk_u8 zXy=L?{QQ=EMIE(fhOU7f1B`WZ512n%ZcXJ~+)7sIp*D5%&Z~+WzHj(T`3uz;TAu$t z@Vo8ffYFupX&pIHp7z^YS48lZ!h1J9J)ZSqq9W?^z2L_m1Pw%^PRP&?c3+%H8&lI*Ll#F%<>FyoDI{T&29!#jM+TZBQVW0i>%mIACkw_Am-*p?c?G#{T zJv|APh0YKF{KM(*qi4%m4>5Q?G#r#SI{`$4>B(~IfgfwekN`{mm39D#bfZfkASZV$ zRroQ1@hyZET(Gf~m6fWxU9V}tu0{uF7i+=r5#T@vv%#!!@-@k9WqDO=X3-!&OKUt0xcZ9k-$O%~yVzs<%__lHnE+&l@wukq8@I&?m>40FR#7AqxP^TRa`NU)tO> z>uqLSnuIicLIhp-jcoeCTIn- z=sRC@a&&aWznr(wH1R`uW3pIX1*cnB)IDClz-g`X#coryWwtLKVBOe19OnV1)u*i_ zZ7}dKUWb{yEp_R%E_fZ*1?G;!uKlA@)XK`Ohnw4rQl`?;^yKesN%F&xkfD*&;~RSv zmD173w>qIUYyt>|{6>Z>3_48OYbi9SSKUtGuJh!zGC!fBMF4}81N zlO}V1MQ5d*;vs2|>l;EZzOK>csL_Xc{hc#%QEzvOaP|BPggRc zl8Q#%X3D~j`kXnv6BKj-@cnjv7fVB8xtx|lDx^~R9*&d$!FmD&dXd^m0%K?^|xc!&m{J5Eq= zsE2qwKizzAa&qbt7_(M7(C|23d<)`YDFCMp@sTu(n24S63f(Bc-OM)`38za#Z|FN{|rXz-1#nv9L}Lt*he!At2$o;b89u$a_$f zUa*q6G@?Rg>VGmN$+M$&p&3M}&?3S4h>-uo2O)}du~$N&|6T{48e@%Jw1b>r0)s2M zhPc=xbK#C7)dA2J|30BC4P3wOd~4N5fo~|iJ6suq!m< z+=C+3?}J=+x`cp|l7^iuZ*U<6kAq^wC%6sTd~cc6i(RqhuBXD7)iAICr>aOEaGZOxza6??hYExmhAv0%=iI>g-HNt% za!fbMXo5H^VF%V!oj4^UR=YJhUMFA{z)5}3={Fvzl zw=%aJmA=-aPt*s%T=8#XfCL7%gXqqI>dfweZ{ET6;oQWv0hpLBjmvx=>!*|CX@A7n z-njX-lLN|*eZ`oxN0v0oY*b091|wMc9U|@d+yW^ma9+9D=>|^>{N!c9z>bmz8Xk~e zf+H0`Bgd|Uojj2#IzYf!A*V%#b*9I1L+=~C(X z=xwl9?-D8}u3|>1(;E^t`TOZ$l%=9mV~|-LO|^N)e4Vw-ShjPrc?EQZ;@u**;+}6ni@?E2HO* zS?TLAROl}aF92F=1b`Sl`LTh@w$c6F*b3)(Y)qIO2`4K0)EEYcx{Ic49CRSyKbO$v zl8Xlm?ouV5`~4IYF@O8W{s$`oYB|bDp*2;;kh>4FpXJX$=0R{HbLpF&(upBBZ~-uk z!|#l_>&$rn4)=(J3p_ zAO6-i$|Js{M zX^Q>ppsYzbtzQL5Bg8p@HDk)J(M_fWMBg`?z_?sO0HAe5vAZMVU4*u&fN)Ic0)e=pO zbUg}DXp<~Kb&IPThb3Z+MWM+%RUE!rNK6x08h=IobalvwX zPG^FwRg%H>`}g!rqFP9y4SOc6&C)cF9szMqKc~`WvMcE`YkP&rsaCRiYBI7|(a4-l zI-O5^urWPS`wF^LapQ^km^Ch6rk2$N_xEBV@ih#Zer^PhwQ{;lqj(9G`#M8E=OOh9Kh76RClhG#!RmWI=MWyz`~&(xvNC)&p9p$V`pt zZ20YqO-*9eb{ILVob)T&XlhDP>6cy@uiZUL=b!;~YPi|}Vi>?q=6R`VhB8v0?Dtq|sk{MJO!0#f>aXT_QZx%+e&_%F(>4j%@^y=z^6uJaBXhrCnd^1ItPB%Ge{V zDh{m4f&cr1BwVycQ?ydt8ZxgZI4^3EOX()TQH-(cwB#tev3F86ChkZjPpZi9W7mHh ziN}sP+vVexnx5@GJIAf)@(5_VTvfn=>jNl7$$i{}gN zlrKdkM>Qlu?l%Nv&6ZE^I)@TVh}!P*xab*E-F<$x49o;_?%6N;tHaoy8!1L}Dr`dZ|A2fr?;%!3XGPmnxn79B`Y) zmJ8lty+FN^VxO(Ks9$oV6WTC%(`?pTk{+;hs1FAX#0htk}FubOoT@)xV9mg zb-snYqb2!Ibn_Vapg{>THeAmbw`r^Hu*%1R0Hyip^&@%0phK0tPb{T>@KkRQUCRSER&o8TOs~-f76kfrAFFC3AlI7yYf&T*sLv-8# literal 429616 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 #VZrFmL{K1>y_F>PN1R&l3xxyqB z7yuE(S@MBgq2IK$j9guL5i!3|ea%3`t!nB+N4>sdm|Y{OBqHp>Pc&n&Wy|E)(<)X($8KP1mLVOZV1VpSLRK; z_D%xC1|5Pus2(sx>$yw`ZTSdH3|G?g@`GOpzb9i3q+lZ%jfo#IhVrjvSPJJIc?k!%XAA(E@3?rCiD26gc$jSj#Z8K z4R8vsn$#H-sNO%|kui_IH(!1S=sTNx(-n4Q813@Xr_17ly~549YAg*!U78Hvo1vnu zjNtfXYju6>@+to*p;5v%aR524?7sQqK%^T^u1v3x7y1el88;E;^V-RDAxqm0%D`n$ zEX=S4QFh=`JX86z>tcQ-^)`6qZpoPHkHXqWoDqpI{>3;__I=0JtG}aw1X;noe7Wa_ z0`4DZeAfXp_jWJy79$KX=|wO{(F+1MCoY;FTTPGsqT(+ep0!B%_kW+W z*7^b)3vpq`k&yIk>i7?6*)K4Ey$-l38+uzwj7B6ooNq3%SI=m{w>~b>*HEIex>Jjt zla0p59=@j7u9pWFkRPPS-df5WtNLz+K#1t259HVirmH>#K$u zL%)j8oAKVaz!u9}CRhcO3?ltHIcF_>NB>mKAK=a)3Ab!+8rG$3IkP1U;!^wyW-5+~ z(Dmm887EQ9oc64WAKOVl@i{e)Y$D{19z~H6)oT>1mcf( z9IGQMqoZGEM$rmY9rPs6V|u{0OxfN%ollTg4B_SdMBop8ZH`X}qJNbnj+N2g#+vl< z_4}|QQU-?<>e=a-CLNfQIw87UcG^shOhx?Xx&w}z)!#nS)?&1E($=3)s)&$vkX9%{ zfspr$6}P%Ad*QS|%S8N?f#Ga6Zj3)U_ZINJ$!1Pq+(o$w^cJ1IHft&+8Ap3P`AjqM zMGfSDSz+$rZr_<%RFZzxwV%1TQ2dWQCAac#Rz-#A7GQX2kcYnP;p#)Wv{yQdN%#V{ zdyj-v>o|^aXX|-Vr%BphNbqZLw%GZGeOiBj=Pm9>gb(`J+j2vwRsL^+yX5au#N`#* zY1tt7NXDvGvBO*AJjQ!TeF!}rUGnP*KXM`HI}Y^}UArr=UQNc>c4lEYALHk zP^OvxF~Z3OaiIfvV4>Mla8mN}&b_L-`kR{C+U3E#dc8vku>3h)X@v(2XhOqNKDB3- zbQFXyuX7{ZsWXVP4(Q$Wl=|O3YRreLeYU8ItlFw<>0FBBk8P|Tb*&(ZbMWL%MM-<$ zz@hyj9XY~2uVEpevbMBK1>6sYrg+w5Qu5M$_O`Ef=Mv}9_@g0OW*|S<<&>URd zl7$rL5&1{glAH`6Ng8Iqz zB(ob%k?r?;fnNi9+$UTD_-*VH`!mb?AOu<-Clp@r|3vDQ>pe1c)KEpg#w;RcaF z)Am{5#}pjDwqC(Psm|+LDU+NfAmD-4`6jY4t$#Fq6jqZRzv;d zc2#smo~~^n(e6wRd*7nrqEfROI3hq9cDs{xIP&B>?RGv$S*jJBfPn}8vfJ5b5xv{i zXGjLnMh7M01Z}Ex1!*|gOoby^S!72nXnj5)_P6^4-$ zPn#rZiZx`2I{^gGEfXB;5RXS2apz`BA>zna+BaDcb z#I#xR2W6Hu_#m{_l*%D^xPZNU_~Fjb)b1QkTn^?)5Mj@K?$|JKGu0UubSg! zXite!i-!H6^W*|5*I&PY*Pf#y4X*t1Ea(@h1#hIDhSFr0U0M)*gOc$R#I^QZS$}w) zJ1KX;W56WSJ#1=io~MQ zosgj4((rmwNICMRSzd^_-+t#-8rA>1sbVU7H{m_C&ol24wK{08!8r+=Yg)P_2VS)X zUEMR9UccT1p1#*ZiuCu&$O0j(u~;WIsETl zxs%%Z&tdMf;U~Q1A?*%^->eKfla*=xUZMqVc9}`U`?|Z&hUlKk7DsqMEZzUq`2 z%pM1k+a~+Ys=Wi=vZOk9%S-Mo=GwWnvLx%#OpFVtHejEBzpsQ_J3p*r%#x=?Obm@< zLk|fvX}yMYb;HxM3bUeO(~&J$a)2M}968SEBGhUzt&|4jv)F#?{Tb$=6v42;z9$48 z_+x4oH-DeStG56sQiRl2ZBvuXO9a54lgm?7(bC$&_dLPp1}Wwt;_$;~D&N%f+>MZ% z)v2@!R`Gsa`@x0wis`3NrwjuU&qZ z;R9ciylj8@t`v^g@u^U*QGuLPAGxTDQud1;RzsV4%UKsD^qx}tAsH((&lp$>&wPN2 zoDz-kh^`kvRm+y4`|wcnM`LPths2VH1f)-o|G+jwrhGWiSyluEH2F8+e6~zXOw`}5 zN(BB-gXD2^NOFs>pOCw2gkYr|h zij%KQ_&tf-2P>inMu&K{^{+i?`70eYL#Y9I1u(^0v5445DfyUNXdS)XE;Ceu2oP~Hz9 z@QB6H+;tdpwm~rsUi>gK>JWH_1y4oZ&RT=>K{bs^!g7{n0%3Mu^dsv;|AE{qsYUMZ ztZ&f)YeuMSEYP_G$ENwU4HLsFtT|Myxd>$9UY}5s9+$A^S+lOh40i`OM#hC@^UQN! zx{WE!C5(8bO)8@B)awkjF(sgAwjE;}FRK{iFpZ`_Uf<;yOqn1!yE({>N`v z%ah~cbllw3C0vk5J}$2wFW@eFJeX@XI|M`zQawP%Zl?4)<X`;L-zq=a3S8vLo}7=%D$w9MZA2@BG$jt^>xa9mZ{NP@n3@Ka8x0>m z2V2tEr>)=eZQ=5JbWtFZ0>lLnKnYT$o<04r(_UdU80!I)308#i+6Ktops#z%u(i55 zH2{kfaN59k1IdSlRWj<_b-UobD5FMQ2iV{P&O@Ft_y79zx3{vb8>RH?Y2iq7)K5c4k`*~10@P3 zrQz%TY>q`Z7*R)nx(5h2m;j^c=s-vtRh)B4X#BU$Z(sl+uzLLWpg6Ag{8RP(#zL(v zU~>@RoZA5*P5dC0pNN+_V|7mRcHBxY03EDAfCD(|!{Hg-a&4=JKCi(Gt;-zmL9-LP zzj`v>^#bPLa~$n3sycj;@floq_vY{ki|2xxve%=k12Ln2re0tcjl}g}qoyH@#DAN< zel){)9m|2OsrAcwWU46^jiBEoWil(i9~{F)6vo2t1@^mjzE+UwLYj2N&2o9T@=Av- z`?7j#yM>l`A+s|3fj}frA#|?l$MfW(E!JQhKHOyPFg6bh?#O5;;V)rE(Of2$c&_^W zc+~O(A9}8@;S0FUeCriVRVbbzW6kUJ{4~d# z5L_4!IrEz^?C9QmyQ3w35qQV}`vGs?zr+2=8(NN3!61=acVfX66t68rl+~9XMg1QH z0u_D7d{QNAS;UvFcfZT+u5i_1&8Vnlyc5BkW(zYxgJNhb19C4fr+-6TcFp+kXplM) zA*ki7=;2P1dA>ZQ-$TIt4@3Q)xnO$K{d7u(m<7D~ZO?A?vMK75P|_ z4~dnzgl=n7(yE1pMfN`}Fc7T*7Q;e=b0nDao*ZF;0|{^l)&;mBprnAI58U^ww-gAxTP#7DJNLk;iP`4kd_E;9_Uk3OJ5D^<&;YG>f}K-g3$hn*Bx{a)}CV zQlt3v#{o_xDGRu4=({4@|SbUHSNC*fW{*-*EtV>8!->qEQ{@ zf$LXnKL|`?Iz9>WOgwTway2oPq?<}{1`C2=&wpopkN0P88+2MyK8WQ{B-A)oBQCq` zm*6m(T;dlne3dS3Xc&lXzBl-HBKWWSe$Vrv_$Fle8pGqdofMFd%D2?RBGehXfnoEL zVG)QuK@SlpN5loFB#?vox03<(;quk&%#0*^SsyeL5S76H%oW-WBo=_s^Y`DwUBlTB z?-MD9bss~C=WQ;*I8DmG5~6Ye2hP+u>vVZ z4h}J_JF553o;{vRi3uw*d7S*Zzbys|trgOH_<#QkMJ@if^~jkY$*Mm4^XAZ|!$x8( z%)V~+@o11tnL|&NjD`qXo#&PM67>3?%|M8f$a{8``E9ADX6&66QN6%yc7gyWO97xZh!(2X0rfuxI3O0WOa3Y;F( zYO1sQ_O|6gtj~AVN~n1LBTC^4D%!{VshkOGD__J+Bz>t`Cf~9M$xd5Ye1ht~wyA@g1XHdq)4I-?<5D0Y`_>_JFIphovm5($`^%&DRoX*F#oToQ zuKTWq_=E~q89bu(d~;qiXES;Sb$|4St5f*$&&uxVsf2_HLcCZu&r3^7C-CK9GR>B7 zjdlJ5i*Q3`TjLSmX4dJ)dQF!!82vdm<{X( z_<#V@sSACVV0i>=4u%rB+ii#WV{gqvpM1)Xkp-`x1CmX)X0a`3c%$m=bacFCXJ^le z=XLi&H4d-t+_rd^SNpa8u{>?^!5-mh{sUo6Pa|EF^$C!CJ?;C|Yi;}hw-v;4fho{9 z{{2OqQSvHy1Nt^L5e3S{r%xeo;OjYW)`NN(^_E?$`1Ox|p%=7Zc^t!o7Px)&w{%t; z)8i=L*Ps)kQ$sB->~h=E8E8!M)k!}l5+0fq{_I6}tYPjViwi!V$)1SHH+g(LR!RiK zt6t>tA-U|9hs&-6%);be1vg6W5(yXcX>d&4pO_ju0B6EFI|nez4RQUrB!$9Z`^HNB zw80#Lp?Uzo?1RSR;Mk{3O7i%liWz#xsIN)uQ_rf3X3nQit~ke4jOy=->(cWUNuldh zdW;?C7#=ai_CG9`Kr6HNdflPK-8!JHe|n;lO6r-qL!+(jOAPN4({$*g*&?ACqp7Ne z5v)ZjO{>_$O9_C2bM{+G1UrCBT6V%0HK-3i2tgQ=j3{ZQm9JSozUW;J}oh48-07hqfK=xdR8D>zOkY_&(h=O9B|KU$w=e zuDyB4luSZpAr*jJSO?ZM@n(OZv8j1eQyd)r2-stAsDnq#Vo?|$r^zy|RCRkI2XcCz zan`!sumFJsfB+X>r4;N}_j(k3DcviW{6H<6y#5rNg+>xa#N7t|WMY%+f}UU@ce73{ z*bCrHY1*e{IzSnFjIK3)b-pKk*p_hKj58X0{=e*9K%QQQQi0M7n3hk8@W1exuwj`9 z!+dAx;BMWyEBOo4P6yZzpxtxu5PAH)6OiSJR19NOzSVO5(A~It4kbB6VR3vKa>JbpwSG8 zxReyxBNKo_Q%C_yTUG+)?of}%vN%k*N%6zU6Ib!Rv-ukT=FIco@Y(FJM@&{$7XPM3 z{qp@iA6VkAJnj8KEdUEjAcz5!V6Yf0DKBpqyrF*dRjamOCc=zMOnl;cZ3;dVJaU)G zZPoX`TJ|6~NhJ@&s8QU!ufO>8l8#u;xtQT*lb_hVIpg5GUJ1YZBN+jOw;_eG|L-%q zsho5=l_ROCI@HpMJiJd$Bf^o=)-{_Yim!qFq$8=g*?DFEi2mp*$duIm2*2pAIj{E- z_7{y{m>2LR;ROBcl~jUE%v<55avv=lbH$F~s_>C-6kL zBHNY49%5OOn{{G9f*Ek_FVpAEu(!vTpQV6weF+t3kE7`LB8lynOCi3!-U);QbxD}3 zRzKwG1;%GxjP6oWD(@+#-PHvzHFfW<6ML@}*=TbMCQZ>Kjf{+d|I1&%oXC3{V<3Ob zhc$c!OuxaRmmK{#*1E@u@vpIHeGOM=!J<(eahny)+wK`9C5x1tbiJ+0T+1JnH3t?o zn;Wc@567k4u~{NLo#JgNyx}=tM)>OQ0`0eM<(3#oO`)WqpQQ^k)pmGTyR7a%svP>K zbZ2ZL;i=ZEDLDUX#(*1JILzeS!E+wvig(!KN@wHC-twn?Ydki%2~&1(_i*ba1E3uA zj6@kXrK*Uo4Ty466d{B}fR_Q#bf@H4xR+!I?(IKU4BfiZp=~~8y(vpaSF>{qsj~}) z5Tdcei*NH5qi?tORc5cUqC`?@TvRj0-$>T^pqR91ma1fq?;KC5S)M#0N&xtHx;2uL zKE8Pr9nNeRG_>t=2zESL0tOv78m+SWV;eFuGGLdj5T7xgBdbs_Y4ur&CPB&+6av64 z$r?RV+V=n<=y`ZY0Dz(0$}4=4yamL1F-VBBLfNBd277+|Ljr0_|7n+82WFm-eej*& zl4nGvgBP+}P<;AuWLXgK2+(p`A?T&bGS5Hu@*R!nig#85j);@GXB?vJhPpx}q;S&c_W`g&g5B zq(h&>%ETTJG}sgxKU|N=G~lA1cu6>X)WrD=Iox}~tdQV9zKlPgJk=OJ9iig()%QWn zgQXvapK??VFY3^5`F+QPwc5p2YTo>uvph6_3)^><@2YH_N!*HW~^ zdVZgl%kq|cn)h1t7xn+clTBsrJh#Vl63puYblQmt+?2b8Ef@?oGwuN7hp4QAuhG>c z8a1gQy(s(Z8RI+eLS*wo`YbC|X|j%qr6_Y~oJtmvvm7d2Y5b0mYeI)=69mem?B;l* z5@8?%i25zb4fHsY{GF9f**0+Tr&l6MPCl*SBL02luvf=&p+NwQfjQ2`BYRx6wtCsK ztCmzk@-A5?kIoy}vq|B`!v|z78!x8rkg{(@`CJ~&a*k{4FRSN}UmiM|In0to@)tvV z#*K4U_}V%w-{$f=G~bn+=3d+d9-Nn5=>Q@L?gn~m6MLySFngH(?aw0iCd3!963ZFmI5&hez|u&Wh*MSStGpt{{1?%?c*pZ`mAvkA-d?|@AKbbko#=7 zV{|Qz^i-T(n)?>xE}%AW+DdY~9u4#80Dz)AL(=4mzSjwRFV0mxv|l?k)%M0#_7ruP8V z@~MiNR?gKU^*;nf@7IEY`g0BDQ2!bCBetI;`etMN)gcCrEy4xjxOcq%^yy@uZc7ANY zRvNsdyiDC=wzjsO`r{+lW+Q;bu@aSJ95LTviqHRfIJEE&0n;pleF7{c1>lmPxr1++ z>!P|+o|@f1r@aw@R~W~TqJ2ekYS;qBT`&iMDa6EDm_$)yqc>wezD*l{Gf!cT!NKJx zMz0)IR)o9W@Zk;(a}~qpxC`*2CK0zm-a{*pzM*_B*wViZrN0giX|6xOCu-l- zX3qu7Zb-=Cl%pwo+&8Njg7PLn7!k`^K3Jh5yAUcdzT`6Q}#@+bG8EFqybftweDcz|`W<2^U>!tUNUcsJJ8n8PY42Of7$p@(L5KBk8# zZ-%&rNmyP+k~gyGJ+e3I+{oI3$o|XnhVA)wbCHOQ$LY5;Fo#G`u;-|yI&3zF*E@;g zWH@VBY}jO-Z7BZSpE?J*w93zU@e;_2RTTU1LHxbh436Ziw7ggpi@r^Wjo5$N=RI8U zMDE`DZBg9l7p3zv_a0r^Gr^5nfl&iv0g$|Pp`Te z71aXFTUI9ge(SuWOiXCM=bcyIzUU=l zEIe9jZJP?%IVoBXnDFQDZrN%^J1K#4{k_Dwo6+doD(QD z|6kRZ6LdT?@vCsu7Qp#4j*wqfe*ms13a-&{oChnSXNxgvGMcHgfuL8^`ePKv2*XwJ z5nKjufK3CtEr=-1U^?nJec3iur%$y*P`l+w^98MA)Jx~XS|xN{|4YGdcv`8mT3(v- z-+}nIcVbcNXPi|Z+_#R6&udh%?GfT^l|#8~j}*$w*Jl$8vZhA&UypI#tC@(XS2eSL zir!Y}JFTdU_-d=cU*vZSG^Wf0pYjINHQ zQjGp>E2xiH$J|*+P|8xG3inz=#v!4)X1uhiP|zT8+3(>wQNvqV;Uwme#^6PYCpg`> z61*wF8G8v8c=Yg`?YMq!E#}(cd;x7DxkAWb_G|4Y?OujzblXTcSfF0dC}V$&;pu zl|PIH!AcM`XCcjT4Bp7_2^0_Ry?J_n;JQL3sIlWvJ+0@^?eFg`34ET0o;iPu$98uOT~RVc z;7#Gp1}; zo0}28k)gx}pwa#QqL=f;8AMUC;;B|V?UuA{1C=p{e|3$C z!(xqy232oS38Vq7HS;Dpf#2-BWQ0a(#9&J_te!D{X48<#xfvJ4itYTd<1=qh6mwYU zsz|-6(lc(0U;`r~Mj`PE@G5gsfTT}FY<@cw9ArB_=vusd)!Qe#@>8+9*9RIrd#o;F zPppHEc{&aa7tbkBz;p}jazNek`zv3yFT_D_4^JxmhLCCP{?eKK;>Xilrq=u-?*O)g zt_vplM}TJ~AZlxC|7#660Z>s1vm8D$w$vcs+9O}tXlh>OaddX3|q+!gIg1E0$j!mw;)Mvjh(2WW7$l;c2xJ22-Vit^>_q2cG|RU z7b&6{S?rQONYP;67hvOmHMh*JQ~e|Pv%Ay^UF+z2>``Q6BN;4@{Ncc&LP1*+O>{%H zN_b-C<9|=w%H9uECS$`&?7|W^qbDy5CyuGS8SE}79FUDcxedr^NmJe9a zbJ5Z3Zk#7%qZVPGY;AC{yREoi*kgKt9 zQ-%9^+vgr1H|<*P`Dmv+_1V*VOvqclDcg-Zmqz3Fr2OAG;wBqQHN!C8dR#}eJ?45g z^R0UxH$#X0wk1x+>`4}_cx}goVCb_9=_pXd4i|BM(5Zkq8G7!yxj6&=0&;lF!)EYv zq&(g-CVp@fnAteikZ#Jbd2do^!jXI+p(BJ;Bnz6~NCI2z(9%3L`)tk9?>_#(pI_4f zo|)M4ddb}G?-)4#fz1vsPZ)(akNo{ZAhx0`P>f{y1hOUUQ9ICp;%K{=-*bna1CqvU zYc0(Lp6Co@P%esyp;H~xenA^PUzJDq6>KTeJiZ`QGy7|TQ0!c_SAG;miJr4L- zRaN(YhSi*P6#DE6Tqp$0^q&^C{_W=oJjh=B3D$VH+ku8Z{>;(is+^|8bmIjE%mvWC z6(h`GDgomPpOln(xwcZnHV88STpAG!Y-CDS^FEZ!eX@Nrh)mw*E18dV?osK56$`M& z3;@pg79X~(j#|Kn3r9`e-BH1X;PZZ6NDpgH`wG6e>(=~u_5{JJ>+R?Kef^9o-rq!v z-RUOzi70*>M{kq$eZk=JNAmll`tkdp?d1sF?lLm|kGdgCX&^>7A)3-G<3^+300R+Sz8OIbf_tA);;8ML(1|RxC7F z?dg3oivI9B>p?lxCu1kemIxi|7zozEbB+9FD){O)hjHJr>D1(kX2-#d(cJP)Z7Bj4qJv7a(4=9=E@)S(llCwySsUI}^p z@`RNfQTZ|NvEtTv-~H*G({85Pv1PpN;>0YztKZ-~AOYXR8zDJmfmy@-IDZr)h#6tNv$Z|0|i0+Z>0ckd#W2#~-o9bFA zY}UePn8qrwY>#lj!4$+$*?IJY*AePt~g4OsLui`?_Hryo754G-!@eItT-b5NW zVJtp~HeX}u!4J&!FIKVmFxZ-UQzKeI$9GeQy+S(qmi>mLJ2vePH#@ZR5ZC9u>t# z&Bam4@(N=`;qbFsq91s7|GZcMbVz1~{;|V81v?Q@cx+fs3cYDT9*aF_m1PCvXKIz1 zNPJ~HvTs~$AM1~m&Ke_SlWX<2VVrQ#IPd@R%1{jd@sdaI$1d%e2kT;o=AH3-dk_Q) zF>sI-q!S>D%^13V<1g%OWPze4+LALti1B)ior9pBv!nmmm|{>Xy6n3w4uO-7l}I7WYc2 zwR#x$!Jb|o{y>MQMk_BH%Trd7D?!_RwmzhqC`M54ZbW9Z>HCzWn2clT#Dn{40z4At zzYCyoPH}40vw3uJKA!+V;FbA|uGo`rbFj?8EO*N_NNm~QKphruC-*Km7@FQ7YO4|~ zfe1lZtkPHs8uG}30(zLjcXb9#G+d6VMUP2}B*l(R*oP}AHZQr_z}MPIGDV8j*3&O{tDI3YshWQT(^S2g^PR|- zs>_Da2Gpz%(Zi9-~cj8N-N=&?}jp;qoWIk+sAevcNS5Kx0kxw;w4FA2>7I2qPK zDm!9YN)i+q-q#`9mv#wEQ<;mn*J{=aL6t!p?>BchgL>K`?!UHDuoKCMeVZuoK=X#; zu~>e>1IMTb3khFHueM$FknJKLTv_j?eH|SXlZ~EuqmY1wpNEcNn>Tv%uua!%2TI=1 z&S$!Bk7o26EABWl%KvxEZF4~s2Jz%fo3mV-GpU+E^)RCEpw?l*3xCyf0FFseAc$h< zfQSH5UeAX2e%|?DmF_wN4(_OZ0~nzW4>d;Wb@o>!PnR%(WPnTv*m1zi$M;`HhxDO% zBN?fohRR0TuD^2jSyWWigOX5Oy1<0x<>fLLRaMn}V5QuFPdVP6ktM-`&>_&-LsuHZ ztf*_F<=)~;zq+GXjDT0<<;$0p)YQ=eMPS^7=o7dRpaabw{JeJ0JAHR+raQ1D;X6u_m24oPQ07sqBZM9KpkDns1-b)yBp6#4M6lkU-|@nqGJU|_Jh zaeD2h5$m#}&k+b=>RTD?suJ9ztk!o{{;vV#5!aA;G?Zm-jLRn-oIBb?3C!C zeXjA#!)AYFXK>jqlzQrJ_O(_yIYb=) znz6|()@FZtvTwrB!Y7q^;S@ZqSD?~DNCLB^fO%#vA&0UN*=ww(z(h-i*Regw&plU< zJ3Hw++t536f;&G-cG^B3%f_C%!zqllp^YINYL0j!UgBm-jiEf05E4@SrH?7T*jt(< zMMQ=tqfTME5rRLiLcAUG$|7)ZafPwtDR11zG`MLhxE_l+zk9;ALu2dR$2NqWJ@O;O+TFpSPJxo( zlqWfMX0rLCa;453meLSLLZ79y`opy24Zd6P6Vg*?!4@*q2tO|~S`q_sA zThAmvi+{aXud?S4j!|lTv5xz}aYC<3FvHkny?+SXK9=I+-Yu;DNH)&wz@~UqCUN@b z)0LQMeBz+1eE$5oX9$D^`GFAWPf%_YU2q0~ zd+tD)!s3$4%gdsMK5&d3?6&MBsg?TeHmy@qQkufgUtTuD!NE~jV2YW%e5f>U-fbMO zR!Dt)lZ|Pr>iK%>$$|xpXT~kw%NLT6<T3wE!TLZqzl8*`sISTGee`t(CzT@KlsCTh8NbY)hN1&UdYwnAdhrzbq zLZgSbUeW_S1U%x{`1lu~uw3s(LB80M2tJIDprw%MO&I+=q%}vj{cw&{L?GCON*=v1 zS^3^Ov^2w{tNNws(4kbNS2P`?VOUYPDT?jf4)mH!o^AB{_AgJvRz!!G$otxwaI+~^ zT7IORj*%K}F#!4fk${x`PJrbJIF({&qDXE%W!iiq@l%FZsVDk=D zVzDfIpePpJ_93ruD=UnVp7D`9cH4BCLFH#@((>#_Y$J~~aj-H)>&%pj5z}ce2!rlY zjU*#Qrq};W*e1!pq>Wa5jA-F!O}>2R624z~(<|D}GkTzc23?u!Hrk6_nv#sCQbagI z4krOxgI<&xcW`|^9v3<_&Ve}(?*~=;zh*VpF-IZ04M+VoF}D%ucU_IsJw=D0*Yeve zFO?+axkRD6Kt@0x4A-b=metq#3b60CwS9w!>4}&w7_Wu06JORFlJzn6Js~s0Hff>luMq?PQvsbXuqB3k1y1>(Q9po`PotJ3S&HaUJT0dJU{ zr7l&^T}#z9d0npus>?M%oks<;1Ex1T8C)%cg_HZ9FkhY4JSK76u(0nMmyFu`e1J*; z0p$-+motuOC~*M14vpq6h245EKGZJ!?--0SrHo&cf+~=NBZkAYVcWRN!|2 z(&E*5NXvX2;#yT*9X2&7*UkI{rcd*aPGg2-Q>y35a^(%<%h6XUNwKjv)pEr902q0Y zGX~wZoSdBR-#(U^)&OBNsI~tNOQn8^EuImzpMK>Q@%zgNw6kTMzMQ;#;Q`oH7JOH4 zUM*gqT>ks8g_ncyOWCk@WBWzc6UWa5J&%2Y zPS)4NJ&Pq;{g3Q_`q$VD-y1>KlxS^5L7JDd&%!fw__H;#XxN@jQDCss`Z|1PN%_;8 z@Wr>&yrvxEbFpqk2P*;&?!%BIqk9WZ)MOj_>z0G*4f`=X!1qow#@%!R$)}Cq1 z6n???PIJ7$cKg2FCtwEN$6us6H%lQV70dtKMYrBU*A>P{%kGz_oL66!K2$5dI1s_x zxmp~oz)9&AjKj$g${tcJp0p?ZoyW5}MJ44!`Lbx-4(YP^4HPZ%>gwv-uNRFi2YV*| zWKjQ7GtvY->F=E#D_7S9=u4TCU#Bp%A?-bfX%lN1@$dw7cg$%^zAw+R8=1U}QFgUg zb9Z`%IJir;AzqvlOnpTCh{aS_|LL#NW|gy4@811K@x{$Q&$EhV5#_$|gd&ZhIXr}X~G z{@x;-x|aE#=#Nh;X+panmuDKSu1Bl;T1)rDl*iM#bg5|Mm+0`$Mt+jhvvs9%PHfk+ zchbniBvr|d5e?APxe%k)#avBG7+&wQ)x|ux_`RMo8T=l#OTI<5WzziQl*f#+*2GD} z0a@)7<@o)RD0$!Sr4P~(8&I9fjhJ(F+tU>oA%syxq_?r{hzkf)5*gQ(F8u$2G5ae| zp8K>YoA7{3S~1T_bu5A$H>zKrN9&l#iedT1hY-1 zl@xW7P{!x4=5AUSv5wD9^j}R-H9R!=+&h#l{K)sueK<{o#KZ^i6aJj8#=~ohCK7Zm z06ZTB#QQK2%0j~nGTGO!U*p*p=%b-p;D<(^I)KxHa%Db;blRS8Ngo~61Y8KJF9_jy zfXjM}JBi|N57vfy*L7^2e;8(G51DW79_pWn#7yB!INuUg7&#D!D+5XRYI6rt-|VxB zCk5Yg5kHrqXJfU!LWDNS^B2S=|B)OYvL~O~E|@n{K!T>81Ga#8 zxx%QXWHEw0!A`o^g5d64bf6F>ZS~|I`V84GWev8+&r5eMu(shx8@i?v5KnkJ;MV3ed95JAc78BF1DzxXR8}rLHVc z;b-@Jm(UUA$U-e-P@tWjEHua8}T~x1*CBPRpl{6dAE`;rlfwqfg+^PZ9UMV~<_zbG5`* z) zw|hc1`^4#=$u4e2H_Ocf`+l7x!^!#bdpCa$Uc2$wD+P9Rquc+x>rMdg3$yM}@aHSC zJyFLaTl#ZTZEt{ai{D8zBqv%acnA2Rg_n(rXCL^NZXQ2Xr zg6_^SWUJNa*j4LPuHxD!o7pW+ItnTCWfslbFQ45*yk8W&mFKsR<4r08E)7V!S_k#R zza?C$rC8wwND=s(Wt@j7uZEri>Lv&}Q2-DaMgU>OQj4Wvey8R#mQj!^50(#g^*Yed zg?a8DFeS$W3rv*5(nfV zE!opE1r)%%dHl9sbT&Vvz$k$VDT%dU_pW4_W#wlR*3qpAb=rZVA7csw&Dn6aCfvXHreTiSzmSd9|ly z8~z7sm@bV_@gu-6Lm~<+KO-NF)X%zkU-J8J>UK9?uYFxiz7>mSc%-onNTOF+u&1fC zl;y@N7)v0%x(?aa`J-R7Vv-rbp6t`FnYd}AgYobKj!GJ5HvvVZ^i$Hk9XDtOUHxZl z+DL0>k$T>=27F<6u(0xrQfeuO3?mW69F?DvG-Mvqvlge6R9}#-)+w;wcDvFR{Mq$+ zn1=seX?oCR4qv(?PeL9GSB6WZk->q!og#Hx7jep&L~b=@zy4`{zd!bU?E9QDDT}#I zkJXh%@Ncs=IL27AtCr|>6Ue-MLME1|U2@2y9o+cdtC#0K-ffbnFD;$=Z0{YX+_oCY zxfj(}%g|5v4Zq(mCK4NYO#2j5%dEcWy!ok-VKhz&g4QOq{E5|Ua~o;(Sej`?tb)ve zH12Hi{ZhZ=TQwazg@OW?jFNX)8k93fMbieX;4XiV>tR@TOW~$SJ1PPGqW${f-?zPg zMCy`fjBq;wA^Hae06>h4(UcX49NI5@t{=7^8Mc1~kr`@pB?>cvWC;if59ZW}-d)@u zR7ZGxne~6L+f{Y~U^*xiBE1*+9AMMZt59fgi}g4`c&bpMon=2zTTPq&gzv>xQwoB< zRe$m%on|SCIg=}C=#D~Z>VSIbJhKh)tH?l=-1v4~8p zCyYJ$x8KGsz++tJKYiIzrL|TX5R;;;(Bz)mp^Zx$jIOfBXno^%+EB+K2<1_FbPO@% zcjgzfMowFAIB8W`tVEmiZISGrTmmQWTgKN7-;I-F$_xcyN)A=xf16T_9;C^r%EC_RX6oCzD12 zBv|mrc_eoW6l&2@yVdLafTwJGu{{^gm3xzsCpYInfd*@})0_}=Lu$;)owE!IeQqgo z*1*5Ou=+Pktj>_^byvyi;M$TinadtaYUQ^?rDDjaTmAcM0KRpy(;U(bY@Nru^UA0R z)~MCQ2IfSNo`|a!?J|_0j81zwsZ$^M6cdY+8-VsKDk;2 z{NpRqDNRRPdcwTn>3`ux4?Z=My1PBY#*mQQuKs~|`z9dZ?!CJz3H8`gZOiNhCmm)p z>I_^_9Vhz{3GN2fE!#Yr)%ts^&xa?yU$GRs)3|DT1{)RSypY9rdQQ6DF1h-b&ZSJj zS{ctf?e0A)HJ*fLd51rfdJwX1{{9GtNQ}T2DMPm33U(D|+q!D7cchn;3AhtZ=^1Hb z`jRkK+9J_l>78mwd-QD{os9d0<${Kcj#lpKv)xkK3FfD#Z4xE8Lm1Z(w+lWHy49@=F=_owWxx2`iUKxTolg zb)4!kjrLfNq$DuGG7Q}0C^U#$DVRNcG0!1)cQ!#_tJ5XH@*6IrSiL5D*T zTDe;KVu7N&QPwT8^2rYig9I>XeW6IYN!;nFVO!__S-W{A>}4SHTz(b4v}Jw`IKF>- z*B95{U|Dnc-o^HhCVq_m5-vo)?J_@sj6usIM<&DhlgdqG(&@`QLzMvJIp@RNBR9F- z6Kcti3T!hiKHR7^W2o8F2l9eZ!8{KV;`O&1hO<{iM(cR8tj38ULmqWr_u4KV%DCi! z9_v4t`>7}?UtMlmlgjY$@N|r;OAXOo@6-MNP=hU(ORoN3d?=(Db=X6WZ9=+Hew6I& zxuThC#<8Gbu7QH~_FO<$T$eh~O+qfc+790B{rM)gx3oo`9kRD~DRFfrbsBXy=#OUF z9L4o=*1$>Cr@(8as57|^z0Q@t{tDw!vOz?|#E()C(emTz8b{g`3=k&$OTa&+fb}u;vZr^V}H^giz*6EeuFWSNELX&p%NUz79`%6JgIQ`}hmZI`XGkf#} z3T1+{1wul?1hwMo$fwFX7JV$_LldUo8FDp3Jz5_$nc(Z&v#BXt6tt@%KYW(reIA?3 zaMf$9`q#zb4u9RlAVkxg&+Rge?>yWT*w^|WLfSb=M`tTd)fko&6m1lkwLZZzh!qCA zS(2BW!b{deo&ASNTqz37&orvgOMF^%-ChU2YLPmXkxct%gQN0Pw@Ip|A#HSuXknf} zIj|;bc15Ph(g!&IMHKE5$x zD=UC8mU@8PIC6%`eIr9PI* zOrs0k;%T?=>G#IAuea32J6*1iT@D~6m-o>ls`(R`K2h8v{Mg4X3Ry$eMX61>0hvn0 zs7BA>`33d$zuaq&s|^V05WhSe$9mj>q8HoGgs)$sa{GQ>o}j|3ZVFTje0A&{3@D9B z{?yXdo;~mjW${ND&cLSlPwIO3`U@O5vW6X0yvOxn`#)~KV-9YOEro9%PC12Tt+(j+ zxUo4Kn1&sdT|GPU-|85AsmE*yaO`IzuzZ!kp2swC!jP^(m8Znk$!|gQoK`oqcC8b- ztm}RMsF*Eu)!{!;_BW%|UYsx24ikU$)^k=2WMeJjM5c2xM^m^A`@PlIbjG5gReV$_ zRSAi)p5;`;y*HSEq0GF({6^1vH*+(vZRPhB5p_ZzrXew0|vtDHLeyx6pb=O zKTHQ+!*bltJ?+JwvOyRlVg9nx+9o}cBqgc;a`}iVw}M8J_Ic8nfQDv5aziEs>!xr` z+XM5ZuSg^-?|8fZ+o3>*z^_BrZH6nCk_*opH(RWUn#YYyz>gDf3=zD$K4+l2BnbW! zLFWpwjzGYMPjnr%tz}+$C7m{8?AL5~%yrjt-S0XvHpsDTLVqTuiky~j(_mOGYXDft zvGq!kaCG%N!`-AdS_Y3b`7sY;Ga%hNKBC{xp-5O zitpKFrgZCk?5oA6)UK@-^m(0IvM0PgqTl|t-?sf>U&G%adZm97Dj4H=m&9x8YrJ5U zTmaSYy!^^<4rp0c`rFer5gIn3e=XRq-pCDne{wGOB?DlFO-H%MBu~QBt=;0J?pSrF zIVxXl@6@qdVq_0Y^KLQAb^U728JVAa9u*Y000y) zY=O8HwXYJ9ZE&-9dh?%*A^xk_mfh9JW2V9N179D!S7b7%sg0&*QM#+_^*@p#^1Iu0 zGii-|8@)ku;jwXiY%O6$Qe29LN$zAyiqqq(f0ptJo+^ojnn<}(() z-i`GWOX1NX@U`h&|B^}~FYRIgFrPu&`9iFHha*?f8oO#aO{lc(q9(fNtt2{DIbgKx zMkLM?I@sOe_zKS_1)0CL`O<{>IP3~M6S+k z5}K2mb7M^dQlR0*UM*?4@KrvN+;jNC`Uf>Z_(*5D+=DqsA*fJbH*x*@QxOHL2JE%= zpmG+#!HsGb=bh`2NJtW111uK!*}N92T!o)Dru|iu_E?D=X$1mC&!0a?9x^jq_7<(r zx@k#C@^Y!D){?-=y@>(8=W*9ws>ZI_k$23B1B=#duq1V zv~|Dd?c85YWuEL?H2TwBUz=1J-d4t(6v1oQ<3D91k{-Hn&YR_0(-W4vIe8QDlcHln z~pi``6qDTy8}=N z_!`M5BbDmk%U5SZ!v?#aCR({8n%e-c!4@agS!On?Ct*yU)*rxM0UZ!(d#j}-R(|I) zxS;^Diw$*<%x$l1FANcWr#3z}M}w_&baa$BWbJ0`qm>KKhGuS`E#1oLw7_Nen;GAC zq~-Jagr*RZmCg4A78^iO1Dj;7`)(m|s?Ikzg<^h2Ejt6JI|Jc?2o0t3UU&KNyV&`R zPO|DZ?vesXG&64DQ9W!e-}kyhw_EdUkycs5?H2{QCOc*vozg>>$j^f7b<=o#Os1br z6WvYo&3?Zl|C&q_{s1cuC#(%iJ)ET9MUqffrcyCA^ns~>4QXT&vI;Gi1G_;@fsHA} zZXwKm&cfU7U95kEwQ|$niA}F0X=bZs5t$|q1ut~~yX6jqFg}?)b8Oca67R-X8dS z$Np(p&9*?cDo`HqPk8SFRK~-HV5L^hyX9Z+MJUaXxP&75yk54dKj2F{v-9>AGH__A z8sA*0bhdR5`d`ZIsU`#AZT^u7yF9Dk^x+i9?Imn>)`zKmPs7yK7M`rCRt2TTdX0Tj zNyEAbSQRgUQW)6R5F2{!I{E9cc34K%P9@6Y{bE;5adD6ew^JnHu6PFl_h*uzV`18i z8*UCch|?tk({p@FexA#PleV6gy*2`W5}NAdk$*N|*QV}Wxz{1^aNh!%#khMoqqDfU z*|HyIxzO{tZ2a~=%Q(UH6hmA2maRdzeO>cTbxkhi{=yRDJ$?10r_|3aK!`oX7aA*9B~0{fK6enY_dRmo!3#d zuZga_&}(NA@&-i2Xg13SX;PIuypC#BL-yB0UM`j`DZ7%fxu0P)1?Jqx%dBdG0+dT~ zX!C)!xO7=sFz;w2^h*tT6fM)!%m%8y^ zgZvIp4C>3Z}1!RS!Tb}3&fZ=;=-m&Gj~N72vr92!iq@`; z4Gk><1K$GepsMpuoq?<$2}Q7KC_ginbWn&2&vU`+ydvBu-QM<&qDp z$h{!_20#4jnzvZ^@^!tx8^&yvzGz77O{OyO{r3#(;u65M_rqL*vR7Yjr|Y`-8|kGF z3d$Hs{p<~mxHa&P@zCd^GuGRz^3nE77n!wE!@qrDc7L#WT0>?CRh|bOJ%gR;1pc8k zruNcWbu({pn$ZLIu~*f|q^v7W=5MK-JlgYC@!{yI{~aSRa$4_F%o)x!&PUR&5WO7C(AVgW-OFdDkTApOTxga>?HJ^$r}r8)Y}+ za!r-)oPWxRpYSV<-4G_iAiS?qj3A{e%C}Q29S2;Wm0|I&O>||s&KoN6EAclJLLbkV zN+_Ly@cM5ICN3CT-(RFnV| zHPkS)z0jJCvLQnA2Cqw|-dZVmHE2K(alTQCGH*b9Mpzg+fXWIEvme1;yAZG^0lf9| z^RaY>c7x>RiBH5ol#%`~bDvmp7AB*ozUJZ+ePPxM1`qJTTZ71gL-*s7h}usg22V6C zr8;&{1oH?8kZ1n$Xu1muEMlpD3E-w`hm_tp2y#LZRnYcXFRFzaSAcTsHV2svX<7@R8%Hx*yFKEj=YfT4b&=L*gj>q?Q>0@l|dI*mYz@d z#5ihMGblFs=}6zl!l^Sy{~|Jv#feu>?VzEDS)pW{Q(IIotS2!T`@IvEey5W8OZEM-@J_P1s#sQ%3n#2-tPZr`5nKmDZViXIR4^dCb)<}BUh?Z@swur z(-V)llxc<{VGb&e^0vu8v_|TcZb+wOHI6khHm1jpI~C3!lItQ2Gjq*5CT=q63SnGL zZZOAobgD5X;6)P7ESCN-hW3b%r}fhse=u%%(5X6h#sWt1bF}e`vnBPRu8X8`56R8dDyYasJt@f2OcukLV?sdy7fOYCld+2vC6Y zK}ba84htg2MD@ca+FrxOg9=Ufeiid^3)H>eR^z4ABy-~tS>IqX0-_g%-on;Ep<4$=($$MANSW2UMU_0Wc-i-Meq%42_L{0dci7eEo0O?_fx{$!!(q;}&9q zO?Pw$q(#snq6k}va)6fh0UicOq|&TVJ}MrVrhwG%`T3I(iW!PM+FQJ?X?T)WLQg|S zw+=@Pbqzoy444E^JtY}#{oIc2Ez9o8v$ljFYBEGrJ8`99fkV!S@?V_>0~mLOFhw<=so3GsX*Jiu`*C*$cFPyH(>)C;}f= zVh>JWf-+&tiM2HKdP7N>ud45iK=H9lH*-4qps;KjwUY8C&J9*eGKx4RhR`*=a3t}~ z=jH^}FS>3XT8rXUCrqayGLe|CneJyQ?Vm1~v;|j3O{vfS{E|s6JT;q=6fS#-AI5q ziIb1OcKxj+ac6T_mDZmsjr#3NGMQHYkM+;82EJOq;!Y)YtI=aD){@RPKMy0#uN0(J zwabVu?pEu;+w^EM5)eK*JWLq)RbcU=TET|z=fTtWlQ$-nUcjXZ0=5TB_G^9vh*yDb zW?@yZbLmZ7Tv9XULGR_OH)k2t;X$hf=AnK%9?8c?N4%n<)J=UEtAl~g-#-|h$iKH? zF>A%SI+(8@4 zU|dfgYip6qw$Y~;mb(7OEUA?*%xgz$X}vpNH!L{-_=C`yM%L<0JE@K@#8QJOHf1lE z3>FxenC>wnl$;kn_rbabfQ?t-p$D8HS%GmH=|ls@M$Hx%uHXJ>_Yxi9_ z|20cJPSf}X1R^V01>K~-pTPdAmv};M}hnZ0aeO-Djjvky=+!D}K^|okF0`_u}py7iS(rH+-2s%bB(5 z(8547LHTlv$K%NpM#Y(gN3C_VxKm#AgbFkEPm>|+BaDXCm0zd&(S*-H3U{+W2{UHe zt4XKt8CmP4CaTg|B-Ild2ESj(DTL1UQZ^>;UQKY(4&j-fXKV;t@A5CzLz$<@I~Avl zMBngyiBKm-C>adlm~l?lt^4_F74mZw^Vb`wqJ$H`%-z%1mweqDU>VG}GrWEP%omS{ z2<4EL^@4EP1LYpZpI|54p=i~o7$>&);x}4k-J4`S*hTmw%wFHs2;HU;9$Hp=Y(5p7e|F_ zD=jfBrJu32UFj7Tl<%r33_?l#ag^C@C>sGwK#*E$7c784vbO*Erao zw14&^j^I3NY{Tdi#vE<68u}$3TN_ks)9dwYAGIw6%2^d-=5gKE`#XZ9;5v3LjQ^%NZF>OV32z_SRb&=N9~cA%Cu zRS8uB(fTw@GuO?7Axnl2n!-PScmRn&N&DTKK5u+dXDE5@)83v9icD0)U}V(gb{D)_ zlvGq^!#NU;b4V`gTXNZ-VS{JBhAEhH5)(;*1f{_if+6Am^4Flc0#>ZAr)LWExJ1A) zkGQxBd#bscTOwSPkb4Cfr}*hogyUk{yA*HCPr;X>QWrDBB5o^KDEr!sW75E{-7JnD zV@vr)5Bd4{$m}*C=DKZ9zN8<%9>zvk0+au^)Guxx8grW;%Az|&92FJyV)xkO!|0_5 z6aP}&G$g?~ef)|AyJ84USi|p^GTYM(ZjJHg5u>6Cr#|krz_cuS4yS2XY}Mx@_>B6v z=e6vG6UcDYA*?4}jPxVvHPf2)Y>e0*M+q~{6IX@yq&}FbPwnnoxK%GQ=aLlBdbOIA z38X0+W?qiRr9+9op1@Qu!#vp?{50kJA~f&-p`~h+E>D2 zcQ&&BOgeQ#%8Xy1~d9qsjMJhfJ&cz4?^hf`aVX~hl?0nPhIk{_gQgboz&og&?W&NQc zy4HYu^#?;5n*XW)a%DjMhZ;>JBxte2rj1Ji>u!O3qp(I?F8(HD3ivKVZ$Ry6T+oB&KOToRW13(fZ00=UMASCLs%A@ z#yfW=zpI_O;RdaG{A|qb?U;&rd0X|9+d-UPZ2FsHiy_4Fe5n~k58C9;A!Mlx+7mXfa6 z2z-&;FwKqn#ALC1|(h$PerB%LaLqzfPh zvG^Y})Y}Ug^nqH)i&%%n^#6{YUkjonw%GzRCuIEIv1#6*B_q8rv;*27GdnxBS|Uo` z)tOLDexgx}UfbTSSmuhYEla1@FP<<|b7n^_U?Jz6#D~AsItJp$#NOv?KPj$cU2i1m z|M_|B#v{osQU*PHg7-NusvRvUlhpCZDBj$my+`e?FdN>W)@!bXJ}Z6k78&FlGP-Wh z)R7#X#VxHh56sL^ zf22$B!XhSO_Vr}!^zDqJ@arM_nG}uFPod#;Q{>sYdg=J-V`)kB4eg{3MJ=z}UvIOj zI(&Whkf14Y>@b>=M^O7Q0|m*8D;0Fj+dn7qQv$+m5T!S~T!yP~28Ef@3I|X3hhB+J z?ySn%4z*?)4*4JrhzEbLc>bm2zedi{CiRiGUSg8RF#S-A@-B_7h!B%?8od#YRvxsT z+}CCEp7&C~Q}f7Q0%Pv?KI|**V@|c!V!#7=p)?}kev_nt@s8RuN5aBJy`3PnZ;tZV zOZR^B+Mm_!x(+I%Jn~ZNVK*jd2K7_MDUikAN*`hUfKEq! z_Cuwi$1p;v(>G@SACmq-}9NJ zK78tn-^9`v#8P%wom1bQFC5Kh&duwB(^WZaAnB70oUC(!N$oH~RJ(Y1S z>0Kcgx=e_EJYSnS{kF3q7k3C{hGC4s)j95#3{(jKAl94Kb5?;4htjm#_0RlP7NS4& z6lDWJ$>~Hy6l5GSp+)P4GvW6$QGUA4h8PSpn^WM-SUH+P{UHZ)?U!?UXuL4!Q7 ziIR(AW(QS0zo-l6`sJ6jc}5Rm=}!jaT0*~Q3AwoT4bjDy&vsNbH?zaaM`m|nW5Q52 zzprytWP*sy(S=g0V#_%Cuos#bdmI%7mtu?zjJ7q-t&$Fj-a>?nb1rf3-#6isPeS z_}}HE|73y>Y8{P@$bL&_HVp#>_Qm0JZ$?r)NrU4i6U}U)`Z^`EBXP*B zac-4aVpw2j$8nbjB)zQaFmIU$;iA??*Quaz#3~P;=e!<8>-xR~cE;16avp7{{r^r-~;XSDO2MCdclMm1}32m#dJR z{`>B7NioO59LyDJ7ZM@`tfwO;LMbcftCCo!vhZj=!I zp2|1;8dxfllrQkMSmeW&%BAbe?=J`Xe~GRT0f1j}`JKWbrShW($HvtlTGMmg*Uz7e zzKdh{jf;!h*Dd0_+EY4i2UaF9SeSw`3AUO{{y`X!I2Ek^f0ZUJf4JLl3)+pwdS;_8k2AgO=ng&lj zAZo4$YpSr#(2}aU!Qn-5#J@oomSjbcvu7CU6^uHXktVoDGLtv@iKGxJ_a&u#GE)#u zrcbHA|=oJ=f3avbzQGZrdV-7 zf=Gs-*+f&NXhq%}QA&o_6x_vC%wofYN9On` zYDg~^WmlAltZk-%?pK%dPy7~Y_Dq{;bxFsC;j2LRl6d?_IP2Zo!HMBdnyNMrgG*UO zQr$dF#+Scni*~DbY8o+os==w643bo_+8Xp>I{Hj%Dg5i3Q~pK{^Y&ditdyQL|5~?H z0j+gY(njt=7SWg&Pa5edQ5xbYu#k!03#tS^H`9}g?K4C7+10qMz?G(#|2QS>!WdnQ z>G$GBM}u*-Bvq{Bsh!)2Y?~0HhgaC*yX~M}I+SQ7v=1pUBxIqsDplhlk;s$i;j$5W zW)aA~*y!?iYg659?)fuN{v6_OP_R~PKL0|GTDU6IF4x4x&NKPtmTE91OJ2pIfJFw0SN%Kdh&6AItyN6Ae7)jBFeRX~QrYuJ~==+qV zoakJI{_OMyRx3nzW8sVs4xi{}{~m!{PteBAVXxO!1eLRNdWBJyXY_C z1;45*DcuJH1Nh$J+uDSGzB~qBIG7(Gcm{#~U#=-@_Pivi3Zc5fO0{FKZc^uC?4TD= zC9ugoLN-^Prk-&_t150lPST<5&d1o3c`El^*wY$4opj>ZqUk5~8v5uRtPCj|Mz{Eq0`d!P=i>9zV!U<2?JxRetE&t9 zHPQ!yaSMcM({Xhy3`NZSR<0`X_vE^K6n@#>NghYm&vX)~|HCzbre!0`>tc)~qSiZG zj8fJTq{hr2n-Vbs^8KLZ?FWa}R^fsrS6s_B;y%x07GzDP?WqIRm=vjEyJY$KU)*N= zm8RPLn5BRw6>frS>i{b`=}iYz1wBRb;#Xt^^#l1MG^&iIkCxn~PXkT8qq7qNL(0zs z>`M0COzyF=ewCV`OJt#XBf&wL%8Y@1#5jRJ>J%&*__Kjcp#CqH9%kNmrB!}vasRdW z+p0$={_0LQ9cs@8D3njG0V;k5jsvfW?@Ho#@l!gD2z`-N{0TbhE>d=epCNVAQ*l)d zezCokhXnE6se8)1cT)(11B@Rn$;e78g`3~T6EvhQP~}~9YLYo5x|>-joGAA^O&?jb zfk{O!$PKx1q;rh2+^Sr*-qoDJnPqOmH2i1wms=xhOm8HOEsTy{r>=U1t%gQmQN;ez z*4&nN+xk$vH10c1ki6O~@0wF^>my6m-$}04?Uu#^_DV$_P&?u3La?@Mc^~`sj-+P! zosVQWfqerQ;&4WW(o{I7ze$LL<1E5I0~0AtsCNxdo9~x-fC87ZBPgQtz9)Jh-9us= z7AlN4;w~Ax7l_0xqBR2$Ue7!E5l|LpiP1I2Ig^gh(aBHWZPewg-G&UyF`g)0WYF2&be#~~osp*54gqHn6wUpII zg&gzAV0bqeJj42A)2Mw5923lTA6!YoI-E3qbK!ZSFA&OI&oFS{Z>XSSK|V>x>3R6_B^*g6Bx71)P0!#X#`!QYkKQ<#L^ z&pvgGrW~l>d?q(!3Y>z>>KvyCQGf;2BN&qyJsRdj=1cu-w;wKlBKh-~K-WNK)RwSY zX|u-^Mv7ZIA7(WUS9NL)K>!zCkih&~q{8i25pyaviW8NXVp6IEZ>D=kPyci)ZliCv zyO)}j$}$N5O8b}6$F2`Ti>jf6hmaaG zDKnzb6S{d3usIM(0gDZ}R)pQm)RaaL(Yc*Re)Sd_1|OgxLD`aSyI&?a_c^$^>#X)q z&EEbLBP)L;pH4pW6P5FQ*TW~ZCEUJ)kwiJZhns1pvnN-Uvo2Wj-$+;n#88!jg?Qr(De{8SLa_O#X>^2M z@j)tGQlkl@%GeoJX8$hhB_--au0qGMEY#The2&aj%u z$&Ge#)>`zbvhP5HU5FWuj97V|sM%or=@-h(SBC{SBT7#*e-xg5_SWaUNpBjRxW4dX zc8()@99sC}d*T;dSTdO9&sd*EDVC0znt8r^A^o1`C!eB@lKyr1!ns0#mC&K8&Y^0$ zTe>lK7s$zJFS4ws9eNym25(idV9wN3^C)KTx@lT{@lsW>`>Kss$hWx3RMucuCK>8e zEh%h}U(+VlbKs)ZCZY0`w#4^MavJ5$e)GW0AzAXcZ-oQOsBT|;o~*5-ybjwD%LOy1 z$efAXx$d82RE6E@G~45q8=tNK20TL}tFT>mseywx4Ap^1+2k^}G_9Salz3q(`EvIp z?i9saq-N~&$TthFzspM>$tR}alvM^W0R_Jdn6+jIUsHpDT(^YdqEqnDg0GmhSg|jO z%IM4~ajQ4EY5yOF{KJpBp1$w;b#3C?e_2q1x&m@Pcwi7=A~04S9Us3e9is|32m2Y6 z=bj-DM-gmoZ_TuzL>dtJxrd`$#|)$!4wpR+2u>qh?ZD<5-IC+FEtydUSD(+-mcd$e zFBt-g04hAz?k=0^7j{z^IU#PXU()1`usp$fOtTxe1JnlB)W8pXQ_7( zn5h3|{v#bZ!yg2n+g@|cw^>E34w!J6HghT?)p6%ZLbL$HL-2C=42l|c*%E?G`N`+Z zDS`j@3lnk6!%HZOI-6U#()+Kp0zn%yMO3*!;v3pw5X}`*eW>5X9kd7`6Z5B}D2q~W zy@1*-BmjjhaqKyw_Ra6a-9H)p|M0J?*!oSQQYR77xaRBUF}E@sPX3ZqfH2=qZ2YC^ zA#SmY_>ao-&+Y56b%r$baMnKfB`3(;Xuf_V;2s4O!eueWL!}r?IU^bpH_xiL5N%~ zFj`{!O!w+|iST{L)MyvYx5qCVj`4!eyuOwFpfU?EQ{kcx;q1DX_-gE)nh0OO@GTZ( z?m_R`CceiEO10UiZA$VMa-56KkYR$I090K;0Cx#m84*QX0D*%S8RYut^S9&-wG{=+wVs*wvQR>$*l&cXVcPtn4go&WGq!?fPqh+Wg&7NBM%*IUgnaC zwZfITqLru#@kqmuR1#~u?N@k-IL>5;UZ5-y2`iiLe802(QTk&{Hg_EDL=k6tv0e@< zrH5Ts)5v4n@7yuXEPas@H#&PMJLQ=;>gJ={gIbBIDkP#WMsaoRQnmz^hs zrV&xj|JjZaku0Yb3=s0(QVo)leoB5()a0}xr&gigN{loIVm+EJq(6agif88E4DtkQ zFoZ7mzD7ny&ffVV?4SiHFc`MJlqv$ln#zRd@8#4ic%mb=R%Zk4m-2|q89q60_C06_d+$|OE| z9x7j)F(X{2S%#Bn>`=&u0rcFef0SpB1$Sk*CQW!Om z9YhF3AKD0ucQj;l?+aw5yLQKin?2ViQ1;0-@r;nT!rlmZK9KB@W6Ex^4`h&n!UO3+ z{-5zGVp0+J2pC3>66ua}a%hI$({2pTV!#zM|M|XRXaNg`b}iFrTUvg(fE?=R3>nt| z>~^0G)!R%^mFpIbQS64G;5Mw-VIK)ZuG+?g^N2p9aPb~JQ=}^^D9kUY?=<(ryKRf3 z@`Ca8{JwL=xk)U+lRVd=DL2-g2$%iO^bW+9A#APuaua{m7Qf5a0N=<);% z&;qd}goe6ZpN?+v5Kqtb9q|p+$Xed)Ao>Tu+848dF?`8y+|v>5e`x~undpht@`2g3 z0n8%-E(1dr)r*mhqe5>nTRlY@+_x#HY*|CF&tNRL9mflSnjXOz*4VyeSqo0hr1y3@ zwq=#Ewa5r9q&x3`;O!U8-`cc>6@whn&wjfee^pA+=x`$9!D$7>Bi<*X&rZK2$E|KZ+R0(ICbs7oOKSn@#b>n z+%7*l`I^2s}>&eL1B2>aRG(X%=J$lQhray!W`>ZEFa$H0EqZ& zo+x(r6s~kSMs4ZD%jX*EYuopq-7`*`Id$j_EJ;j^Bw?;~%OgV?`B2S#7O2ztTgwrnbt%N z|AON>c>pJr64A>5w~eU@BR9(+OZr69^2i3a!CAkC{W2HN$v2dbwDGcSmn&>s0s>7N zKJCn@&@eb!q6%JW%7eyu6;6bKth*oq5q$;&HHanA%lxr7p9FqHrUS_yVXFS|;hDVj z;Ufzd`-}bk{h>eS!yDYso;{0v|DFS^JMl`weZlvIK_HbeH5P&Wp+ox>FRZG-PXX&T z_&RfPC}HMiZzFxe-{5K}1GI~dlhad=x;}*44x~!(OF^o1PbeD1o!METa;ePxJq8+- zoZXk@<5t`=n#~3&o-ZeP+weyPDW9;QzQB}CbnUxmlt+EmUDoTuBbptQDy)s3QX)Sq zT3jzy#HwIvT`4Mu5~O>ZkQj-@U^uIqUvP16v;hJ+->k*G(V4j=v;|*(Ou;1~lGz)M zqQOHQ!#={u-+Ad0vGWPv7%*Lt_-@@M9X9a&Rv^SPZR;AMF9GBP5Oi0i}FQWI;=?ox}L;5Oa1d9Yh;&ZY!WQ}JY! zV#(~+hF>q+j>{e3l_tBorpF-~xa8*JyPGCBSho=HH{*pa2Rpl{aMD>{O_}8g1^0+l zj&k;#ef+?hZ2quj!_u@}eHzV%VPA~(<0N8UPA!^vws<+SD(W4FF!Q_@^Vq4(;b1&d zW+hHiCh7_oq;SxgU+ts}D{7!6>MQwfUj0lCMPZi@ZlwT5J>Sd21aBnO_&7Dv3)z4Y z6mwB>@pypJA$J#3 zPoRC*`S;5Rg2xgCrNK|V^)?w(#a$C~K(7c%Lc`;2>i(Mjq zn}~^`x#X0xbmDQ{#}B!z51AHAEU4t0Qa%3#>?air;`1zf8b29C~t6%JM`>CH^IE_;uGYF z5598^?aew5?XR%;r-IE>nJIB+>c@~ojbK48`sGW^Z!GOEkFMnD2=1D`#5;(1WFw*b zJ=eU=#;WFGh_hxISA$d_mY(O5;yb75o7oOj%i3%sBo_AP<1OJ8f{a z@Y;cwCiGsgraz8Xa9W=!)y^0KKlD)FBaqGiYyiw;KnH~76)}>4Rgh^b2x|jH*;S&x z@9?Q+U_{Jjn?MvE3wt2`C~`X26H1ZHfBda~xis#lR^H9f+=DT!WNM`OW?V^{phXXk zX@-sqXJKogT1Iy5aSTLzOo~Vfs=>c0#JdY$q_q!Jd`2(3y+gs%1u&9-$CD)6?vK7{o05K;jLV?jQ0(j!4x2MecalT( zB(jOu3WnOm4{NcbTCEE0eCa+T$qFLuCV4tU5AK%SxfFAp3zB%fqp2x!WlD zVd%8b{2FJhM%%ajH4P?$vmU-SoHGG`ZH}#AtD9GLd=npjA=T~wOZOaz3^cCJ-%(l` zzd8UQYWbZUrE+SmEt{JkH`&Ic*kd`{oR36J;j*9fm;<-Sba%;u+hx zlVDDc5hvdCTk5JqrcHSg-|*bZG@@XCLT#rq2V}&Qe3g>7N+Vd23Uqj5sd~nZclI5E zd_HWeBWozSnEz7Rg2Bd8#C0akycE}dH+{z`MjY7YmFzcY=|u*dthO@DDM0tQPn6JOGa!{U~>g3x}_g=8EoQ%YJr!E*Ebg8M-M%5oX7 z5!d`fW%&tIpU7|bOt z;ae`{EK}+YkJzU1G^pr504s9r0?2s3eu`FsGEOO4B%FC(e(51M$eaX zZl1*ws9!&vm=L_2IsAC(^6FsTSY{4$YGG?{vT9J2sVi1kY%y4kcsVSTqz98}Mv0=} z_R92)3Kg{>6^bcIXGP7Rvk7+Z9WYW)k!;oq4DTX{S^}DGPnGHlh4ZL#)yWAtNoR!OD2sh*dr|18CwFo}L179}ENDrFGa_DG%n_ zr`ZM+&m3)}WW0k>%4qV;_bjcobDe?HZJiw?x8Yp=ILcHd!|v1uT9nLy0(td{)~2qN zEVkEYYnZzJ-l?17^2hamdv^`oTiV+dnjW%Wl!29sj-KB0_nU_Z^3Pek$Qd5uOv^WX zhZu4F!i9-w3>P^K7FldRue^Su@3a3L;grm*gy8$y2k$8V%jg@9h4b;o(+mHb-NU`F z_oa#74;^YpqEUJv{?^67yOvfBAyRprfBu*znDbysK#<4(dd>gG%|D1hTvpy}-SlT& zZ)9;kd*(YN%93-1O;1n%Uv=d5Y4lBsgYT9O2^5;!vP#n{3#`(zr__7c(sai9h5g2& z$&a4dkX#(CVRkwb^8QC2AQ^f>c)QKL>FX93D>~9wt;CXz@8QD?4Zew?F0Cw9p2`YL z?P`HEd(X<)4f&s(_J8`?@cB7wj%Dt=!I9^7;a%JhM6Rb_uNd2!*akJ|a4%bft*#9X z(!#<>Yv`1UlUTCGlM*q$HTd>Dbr$ybZ2GEH^lbGpF1c8t+LG?zYAY(i`tj-OpiJ8I z1X{4c}x1Z+H{iNtjI&9?fN`mP6_irr!8|Bd6FuXr_jq7hC z`J?c(vO^wlr(4$=GK=+?g-w$DdnXxSHpAbC|4uDUl2ef?u}8M>L6RcL;lGm;XCMH9 z&CNTVU`d0qv5Y=_%Xgt@#QwKy+T`tNVVV)t@D+W|R2|lPs~0{B&l9HQy2#J3e0OW( zE^JrVSm!7%m-|7$GkN+B+9t51-yWp8w~@^^2!5;Y>_)JvSJw zs{dR=tf8Flm!)Lvp9bvdcNZB_R6Go0t6uGROZ7gt*=W{7m<>KUZ4a7NkQ*57>n5HX z-@UDPE7OtTy@jm#=$V88t_P+4$a4SC!>03TT&~^9+C8JhZZ014g@wibAwn%71F=w@ zh4`JVsDZV6C?X|3N!y!uJ{PFGgXiY~!XgGXTxk=o>%VV)*!ctI1WM^s84lw)-kii(t)VdgMvw|b!@;CW4<-+y9 z#wn078nzc@J9SkLddh~v2889yAFyG`YU2*6Bk`N70x6R5l;_p4h^0DiP29FP6L_5vbh^_nt9Gs88*v& zR0{0pUaD}bHsu@hyj4n^yP}Jgi5WCU|H|&fg6T>ksbfLUeKfGdAo?F zq(R*Q^9Hlp(FRKt?O@9iDF`L(k7QjZAev>>O2-t}&V&kD2Ir_6s(zkTNEc~Q6Y)O! zW;MZPgwr3R+{^wp@>wcRc< zy$qF>l0xKdV`Xg76=u|<(|5~XraJs~zy0x$rJ}96OZw)T&ccuV>v3a}R*Qa->Pak_ z>4?Cc^Skg#-93fXvy^+{sILnS+x2nfAz=5O zT+7WEjn!+~c1r1-g|FgLEj>MU#aMXr26gj&v+-gKD8p;7B*vd&oXVnzYvQ!$oclg! zOROV!cNLY}?a`ULD*IV^Z!hMnMkBqaIdfk{jpEnf`-HypsV6BPLtm~I)tMX&yT9Y8 zP8+`RFRSzIQ(*bi%cq;9!(^i-oV*(QEExKSNG*&C`p4c7t3DdfLWBt{YACKN7l zx!resFIl64_tf2j>9VDk;QFua8*+nEXU{S9i5%YxQJ2_l&BbspHpl$gWH`3`O{ChmSLWjmt(?GlF79WN7U-aN_GTRHSzTkG$^kP)+_2QZc(8vt#4< z`T>^d0ZI>E^zB|#!gB>W1>(&YQ;=;;kEp$19kO=*U?wN}spr7ig7I z`QAr5&B-QZz{vDypr#f_n-wis(6tT0GDSfq8oR?5a@EQUlv8A)h}aA{0YUw$Vi&dM zUiUS1L-nU`FC6E|*6}w#ugrBSIWDNMsR>sVB?%Fx=U^+;d=rS1>MdH;`Cc}zRZ_2I zb7!{rPKMsT)j5O{4u4jCZPO1^k*flWY0Hwc-Cxvo7ZMBJS zHI-u`IXp0^SFXuVG2mfQ%h+6xvvhVF9Tb`vciplv50V;L4c!Geu z^!NKQT4M<`*BB!W9c0FmCQAB;zWPn`zwc^1y&N0EcKG2H(j0BiS}Aq}_W9Ghk={PDV&tFqj3cp|2IV|SK!-J&k+41m3gUq#0= z_YMpB`FAP_pP}v(HPZjkltTq`*0>%OHBBK%IlD$VbW97*vGxHiJ4Sbd5|(4+`13PR z80p0XV`<{$Vr6M%X-N{-X10G?@$AoGQ(>VK3)>`r{NCQ)Gibofbv4d;NI!u_Oh$zj z6BssO|I2!GZ>~XmHu%9W*SR?gaYTWJF*w5vE|(roPS5&4!VNLH))u_zJ3Cx!0%#cw z%s^B?NH4%ape_#`ZEHkG3xGg_2oI<`9&m|+-q2IH48!)dp(ohAUaddgU_L~=AI?ql zDl@BFhj6x>?Wh+tc%}0H->Fow>pL&_rPmS6vuQW8Y^!20O z_I$1Q%d!7&Ug@liHhmD`7==wUSkmEZ0#yZN1+2x!d3{K+W@#zQA&(mCGpCN+o86x*SwWQpncW3mN2FYWhR2Kl_;XV;m;Lq zR^uu@f%-@?N)C-=V6Z6DNGmzh!`paAbwn}Nhkg`V)(q@tzrvL$DIa~)gwxJ??K9!e z4PV-Yfp4m}|F|Uw1Ff_vE+H={n2gwkf7U_zx&U)9W@JFv;*}3^MXqOf@G4q9aW?Vc z*Laok<8_fYOV zg5il^#$qWRSkWr_sU^^1(jKxC)>AMzvJsn{oNTp)Wt;U1>l62_=}$&uTdr_SfC_kM zd{4rKOn)8P*>c{23pZfoCX7KD7PQcC_RbuVsDO zX*CV%pgHoy_Jcbd_lj9nf*u0L|4(J5tA~5%8c>05UJtGmsgfqPKABD9Ft>9}92*ozDe?#b@M?|Xpb@Y=tCCas$!uimbGBVS1l1ugOs#@)QE16NAFS{ zjO|w|*V52)@ZFV{&;l);VE79?<5Sb^m;@Ku<#=}rYM(fxyY!aJr!35sf!jO}>`4WA zm>b2YEaS1J^S@x*yS~Y8ljC*i^QJ7XB@j&h_$`(2pVMRSdPOAJ7pM7O38QqV%gTrK zGrwBDl075)K}u!4tO8zXef^#jUu<&W??A}i*xCaABRr+Wp|cJefw_tAwVjVu9>8@5 z=zW+RPbQp|fad^$WIq8Vhml8t1`^&v!mko=DTBHLA)nLX;r;!%d!tt9EwSK>>hIK-{T_8xu68 zR)*T~qFDZvQ~n8e`abho!~xF#K}Y5yZ>#6AXRdlZx+1a}+qqj^O| zVWLGbdRkgjDBl|6ARG#awzGeY?StPM>~LB_4{W{{Uv4CobH^IVJP18iS<2xqMUwMQ zQ4*yz`mMQ1ma;%F?K78*DD+$k^}#B4Vy)kWYd^K4gX}W+;thF&M=J%7<%{R0-eYzkP()e8p}S`RAtg!BpZ| zUp^@!OV9aZ4%m(e92k(15K%%%xkF_E?DtrDwRn+Q&KGm+J__+-hlS?QQ^y)*$D^qs zZh}T_6tRFc*JArJ+&kB#q>{dhTyAdNU+=k6l$(tlh(K48(m`gqS%l7hs+>qf@tLD*=PUK4B{TZO03pv!bW(&_~)l90_q%zMyOx`Wg)@^e__Ud}P^!!If zT=x!H1k67Xsnc^j5;6rR5B6?}JR&lZ!ToQVAL!{Ygm|GrY+oqacHm&oGA%4rRfs>~ z&wBRPUr6aDSG+JTio%{0;xC?$-VmC>R4hOTH#zujo}*l>k`<^hLKy^zGYBqkbF&<4 zG6I*1XoLW84v2A|JP+v*=6(oKqli_#V?tLHaJ@QrWi@E~-W~C<$!(h$VTuU6OJ-;{UB4#+}w0K%g4Y@N}_oBTVjlgagpfyeV6$ zgja?Bm1SlB^)zdM+L|Hc8Hf3h74`*SXaH>F^EDk^-9G@JUq$rO*G<;X*o6y9U{~*- zZBbZd{)V15W{hDfUJ>AYzlLuP1~uJiDd%lN5HTQmFH3Et(UDL-^3fh5ZXh(l*+{_U z6pgLz2p7r>EBSJ}7cgmZ@UgJ4t4(@efGrg`{9o>M)Zib-u;9N%eP`F%Mi$X4O|t6p z=OPvf0JmS|p8QE(9upNGOw`rHJiXD|eC^1Ry)Ew~+qs-4+mLKq$bXXB zD((rkQqF|v@@Ml&afIp`Diii?b_NhEmhM!b!%U!DfAChPj9d@Z5GV?s&kS=IJ6}k; z7!`&H?lEFXfbf=N_FT9yRe`R??UD6oNDi94V8X}1cRhFm9xagBcp+)_CO;(x%Uo^d zaYg=y{HrudKAUix@$u{O!rc&~Z-|FfEtnK7gO6k-?PJgSK1p}aAmmsQ5>p5f!nl^f zQ(iE09=8lRpM-&iGCrU#??y?i|JC%7mvl=&2vQ){Mx73~wfbdL^Uz8HT)tt*zCX%o zuy2!GDLbg$hPi;92#uL6T{thG?c$Ue;(T+W8$pg$ZKs=T!KhbG$46+0s1GeQ9^cpDr}~F!VCuJdrx5X60oY+bj{R*j*zRSKvh!?z!ef7* zb>bIbRWt&t%P{l}P{)SkgpXD>SB)o?)zd=k&|13HV5`GR8gjd3^)IvPX`isDioep1 zTUqx{lzLr2t2N%s(nvF)*r%d;);~DmOK6S6S1PRfm~~?OsjlAkYpa5d2MwNJpdFLm7PQRD+xIwQj6(USj-@Tjez=z zqd|1^fjA^UKQl^aOqKhs}f*3ug(O__WD>cK>1A&4=vZX+foL`(APL&e<1-d-g3 zYPaFn>SH7uJ6Vdcv*9SivTUlgD`iywd#21@DzESZ5*^hRHO3NMZa+70&tJ%iQPW;V zOX;C*-W|>Y?|5=^fR%0#MRq~GSBV;iJhJM-39Q{H(-!lVEs6#lqC9zLP2S$#@V@l% zkQcoLbB-l0G+^M<;1jP<7q8GDU?^|4-nmyd6IsyY*^Urn1q9AYEc5;lre-bK@$Dml z(Z65UOjq-}AXpawYm~hkPvD4(Io)R1YF9n^;JZ4loxw{fS0B_Ds=O#Fi{8SdjlsK~ zqB0Ck$m|3d85m+5CFj3kNnaNYqL;?|%lw3oEr8R@g#S}^ z9?i{>0lnrKUwJDBL-D_5;fF*ox@RD8|dVxPeO-pEX-7e>q07nE+w@vVvpgZYi8 z`BPbYZf0{qbn~Z-g~YrQOmQ~qeaFPQm7VqQAM}b>s1v1|$4;oM6=4|$_s(8VEsy%7 zpRA>2q*(1$u+p2Sedb&EeTABQ@=#A$^N7D5|Fsvv4_nQQQbAp-wWisA&aoG@zZcSb z)zHL1(ijrHU^ckg)Bw{$+ZRx#dj4(CP5qAJK6WnC~{fqvCDa^GH zkr0dX%T5Q%l}sI|XUDQo*p-s(`JvrCC&4~`ie;AAGjfk|nPIV{2J?wrt}@2Q3Yz2x zB?ZxJNc_s_oHA#3^-tE;PF{!e}jNuPnJU;FQNJrUa^Gp~4s zjk2~gt>By1@j48jlIil{NDO;5b@l`n{FVT!jfqvik`CK1uS%Hgz0#ojSBYBMx2 zB;CPXWkCMCM5|AJH)&sEq_R9E=nDffmLX5T5Cb7|ruaxT4Z3{XXk}CkTFy=tQrjlq z=<0ZrJa20>^}Y@Fc2wiSJF@_}-nga|FJwt1+ktTIw49ZnbOWKuMg?ZSQQ2dHpIz;J zd*tK!(8n|RkNi_{GUEM511*zO6bXhp4V%%8c3z_`b;#0p@jidjGxPH@@{YTE2al$% za8$?OK5Pf{T- z5%c6Y=KCso$ot0_LM7qR1O)ffMJIoG-;Z$1=B~HTCR`H55Jyqbl)2grV>+L_*V!v2 zXqBN5=(Brk%1LCy=bhxH8#}&1WF)wc5xp-OZ3l@AhpMxYNk57F0TD9<%4nTg+OJbb zgpCJ|Ewe7VmwrVmjKJ)isy4X`JFN?~v{N7t(+jBn&XKBFuOQUI@4* z0q%&vkuF@W@|`{PU)$Y-VooOzEHGB|nLaF$i>5nyHn?4po_0jlj zm~yYZXhm~5yYHDjjs@_KH(ye-tZWQ67JYQhp17YEKel!s#tN;fq&f)9VGJ8LHJL8|C7|gMF*sJVa?!<&O$V!C=C7ArC8HO-}?aX|L z+~9h=3YCjgWnax8xE%mzt!sREHR)A1U+L5Aa7cvIV~{l4Cg)3el3+jb&!6dl?>J3& zqWkc(rLhYM{XwZ*(Fj~rB)wCncbEtxz@Ur(jQ#xi0Dkb1tIdCRcZ(`3qg@2c2Ol)* zVqm$x%VFc_=EJAvNDj7lM_WcVzfM+>q8ovq?SA<+6Vk#fC%y+qrEy@Efy43X`9%AV z0k-zlc>b2^|cenTUR zxv6OgL=YeyN@s`{YBajATsQ-+OT?E4rq)ZucI5iJ@+J*hUa$W_Sqn(9%QxTJPaoa{ zAvRd1oCK)Sbv}LK)cFL(-&Nj(5bVbk+KEwGy*#10dyacVE5YdzX-ea}5rgd8ADQK- zw)bmfCZWWI-=TUq=`+{v{&gE8!ccMCry%dPzfo4npqqgcrsQocrBdFWx>nKz8C zWf}U3=?S}iF~2&?j2PNj1#D){Li2xv>3Pw$4adoj`p;?P!29VU!+>t^cZJbMJ^dCTi*CgxaL_L%RZNyUec*lx%jIc^mc?RQC4+x zTithS0$?dG#caf~FAb6MvG%V5;0SU_<->nJIq7##8k6c{_Ig&2tveYRsL?3F64Y`{ zMpIdf#Qv?TgG2tr(Ud1jl!%96(+m|ek6qNnbIq$QhW&qbW8*vTSoIh5POMgH=Vg0m z9)(g)+5T%&?B6%}ezGje@pIPQyqA>kardA>7cWEM+oz|eY3HX+xM|OU3xWu_Lb#*J z?+SiGKU>$kTVm5i1BsqNU|qA7Le`*vU~hN)!kH z!vVC~eODjYk4MZcLx`&>5cw{`eU+M#k*~=tW!|~dhTFc@j$NSBd?D|?@ZG!?@=-u` zLPS!AfHet>70^W;4hY)e5Gr!{`~KMv_qMOV0w;9L(N1~}qGd_Zhb6?9hPlJ8-$~2o zf73RaNzav(qMLhFtOa>^u*Dbqy*k|4p0xEIN1>n;BrF;>JD4lyNtM`6ls zaX4xjRQ^G<;ENAn``T^tT`7?=bmNgM%$_tCTD$O zxEUHZA9>!2X(Qa8a`@>bddkN)r-9kq!EA~0fs&}lUAvhH;nvm#v`uw8hvON*#jk79 zYG0`pd{qS}hzwmU4~zM=ZO<$|3!(XpeGj{ymHKp6k@k&)T94E=^^-%QKMUGz!ORzRj(mq0?x}bo%6|3wx9%xbKUABG z(UwLHcD;R1g*t}%fr^b)BmY9BoZ5qUHO1k>g6-m_&Kf)DQ_#0`Tq^w2n@lUx%y9Pm zd7r-!+u8M0Olc|ZZ<;i3o zq?boo?`>V@%Qtsu4Nye7gR&5`-*sA>&6@cz?(KNw#6v|3}Ct zuKZ6p=4jhaT`pF;M7Y-8vDxdeH0uPC9qLHof$n$cxModD}Of{t`^wP16g6i1&CHKG_rK!+ums_JoSAW0n{v4oy@P6 z#)>m6+Z_udBf(qE;8mR?ybRF~5Wd`#@H>dl+51DeSM$Z{xR3<`Joql?1*YFV69@5g&PdKnz-?NSJ2tk1YfYgrYyzJt?^&? zkj00ub5S?0$xrL`HnSh~IzkB}gj5i_$;*AjFkPhE7{?{+KhbKp^#p;c0!aX}OeJc$ zYD`8O0iTSk-&W$WBS>0u1qfJbkuZPX!4lOMT+N2Tjzx;7<)8b$)|mm_=HGOw^QU7c zld==kbaS6EMgV1Lr{*iLKfsgU?5oY)G4>iIq#C7fy-rQJAj zaoBQGFptu&do+k1TC?d+hw+iY4LgFAHiGoIx?3Vji(F7&oT0|cs#4SHQrqfUOO97n z>q!L35qx|aAX)`&s)#hFUD}1xio2CJQc?kKA46{RvTBR0fz<36egXX?RW2$~d+q-g|#zNVXwy)Vyds@I9{# zy?lc*UGkF{1zIcaP1Dy?TKGU7mj&GWn?j0G7n)s`^VzaFW0!T_HO#9_DB|SGN$9+7xV?dh`W%+{+q(q` zcpb0K7T(+`d+~Sd7vI1RQz7fM7ZT+UB(o3?!f&oO9hJ&7dpIg&MYf5pkX166L*;7}9c2kAM6A41_pD&;yv@ zCV~g^`Rj=)PB3^xlrl?VB-I z{))U4Hr5KW6MA5QA$h|CQ`_|+3{F9>`ProMogal}HQs}x+8kGeN0jO3RGXKp&hhkm zE;l=R(}wvMYk@L{G{ z;$x<}=g*`f;j3?2A>PtF}#VO0_!QCS+5Xy!^L=|K?A5+k@LSfJaKJn^o=Slri?PBoreow<<;ufojj@-e~u21^Sng4ZG-qsDm+;OSI;iSQ3#C=EeP7-%O1+X3SSi;L{`+jFwIUZ>i-ZAhm5%QGF9QUXPQAfK-w{Gf=EV7SF)wrx%kDJh z#)0^SAPLzE+C%0I5siYQAw(dl!V4E}t`;WRLi!X>)(;;z`yEb?|M_icSHYp=9xP?D z&A!d~n5xAlc-+2`+Ad;4qQP#&k~<|m)5y{BT{>UUM%|2Q!%vKQK~nRzRm;)G+&YR^ zKI#j#%<|8T7k$ICj|YxgXyX ztZ1>Cm*>acV8wVw-L^Yo)wp4)9HlR|^r^2pG3&$rOYW>G!J4K7wFs~2mmM`aYJ#eL zJ`yaZS43h01-T3FANhX^L0w5OREqAF8PDZoVW1hSZp`wmP05NPpYUyUfu!_1a)xDJp$w2)%_qgJ$7=*eVW}@g>|*@f|fNoLE*oA8py1^GW-}zvdFq^d;Su zDX5+N$3o$AwP|(-&(6<8I(%Us;=)0>cUtt(PeqjY1R9AK-_giEcKdVtx;@&0k*=Hp zGwl|${ND0o)|+d&^dkSYnafiDJe9xT&@=-iaZ8ha9pf z3~)WYCA^e_<$%#TYJKo?!Sq$5C@bEbVd zS4X(AJL?!@WhCinN0hLtIIb&Uc>rQAGzJopkrisHX=BbbQ17iZv#Q*c>{dJr?H#pL z{iT2cg46QqU^+sS1@vX!uUm_tcA+VvCn(TO)!RI5ao=5q2N)u@1Q9l=5sEQuo_Rzp|Tpc?mjMz!r~gH2tBoou#tta?iba&oxqnz6@Gn zpDFIp;U|x4l7)*}|A8UDYpx&u9^q4^4^vvbK9mfs8_iLt8Wk=7g#kuVp|VPvf*1v09QQHUcxf>eQylt>nYn2PSc*L*d-g-` zm)dFC&oPzSb;>;^Oz+3?wO8K{#6a>T!f8U8m2J2k{c*>Bp;=GCAll!i)Y+e~W~_@c@z?BC8>NEG|2I?Awy(SVRM@I-?owz{T@qpW*o*B>mLSw zYZ>tO2(pXPM_03t1CI&={qb$yiYl!OAAc`L(6zE`E{kt`;LJxbNqI)ia@0*SVY^MS zmTTHycUdf7>_T}rA%b$kMmE&Uoi&)o5H6lh2(hf2ozsb@o!8;wWQ{#G!C81Sm}Z$V5I*oZ)}ibU4B0>OkFwUc^;r3_Xu zA(paCoO(|=GO7td1cL8iY0iIT3ex*>jHl*C?bg{!WT`^{X%B_fDHpBW`(jf;+K=bv z=BOifp*754PQx;8RRcYdJZ~*Oq)KU%1sQ+}<^Q_BqzWdQ&-xj_Jp7ARX~;ScNC89N zmeXE}sE=8L*FH}diqG+YOpieh53+!V^L6JnP)@ zvCuFhR2}1YJ!o%0KP0ITWIXQtwZvpr0eJi1Q%3xroO~~GNX6TIfiD_buO3R`NqeV| zr@UG-qqh!iYQIZfT@!FU&~RP=8#ue>Qv_%qGe}#xHVpv{Sc>|MoGzd2Z~Jd#NbdGZ zn@%G%A3y$Jrt<#1aUNHM26_LtB1=5hhHvPqm9Z)0dpx_ejVxIlzDY5R)WH_tJJlkz zrsIz`7g#1<(P|G*GHF|UhVNvP>vf&1J(zwCmsVFv6Wks9bY`|(LP zcOV)=3;B-?x|*^4v#r^HX_wUXCCtCl&mUJLUpwpkF~Ql*tw^=*7eMX(sJ2|f)C%)Y z-lg^CF-|y{e*DVYYQKbWYOdME1Ct#7-jgwd2#I&xPBM4f-uhGI^GCPzuT%IjeC?YP z{WwXkio3S5I&w`~tv_q-R^vxJwT-lKATcHs3(c0)WD;B?uyc@Cbs0yCde zsY}0c7^@?7fF17*=~L))K)KJMHJCB_95@gsvE;M*TrawTh8s*Nj{QIggV1dF7AqR? zPK$xVEf~*%MST@QFu{2H<&}jE#~#NXCtIZI+@)NGkz(7=)=J`XJ~^J##iE!KW;*HhJb=MfC=jaX=6HNI zKW@uml0LduIKi?k;~q&ZRzFTCw~CS=%#!B%A)JGZ^bY3RJ9A1$ocOD+v1)uj9*rYE zTDc4le>cyaO(BkO9v(bWT9f)Ig-Hn;{Syh@nOk5kr?{lCv3TIa@!=)4`~fLu%zOQ5 zpQ)+EDFjO2?Y}K4G0?+DQUgxR_z@tZQM`dhmOB)|D$&F(3RphWc)`X zwbb-)w&V?lRrq1NTKlsYXo@L&jl?IRSSRIQd(|5henHe>mf^x^I9 zl)63u69wNNHAMj=z0JHC=oA6{=n@c10Jb>ahISzTg<`>hZwFLD?GBy;uN&0KI6prR z)xf}$adg+&LY>Rv*YKyJ8IR@Po%a$t)L~p*)#8Fwy-nYp+**Dfn1l#(Wfnva*e{z1 z`Ry(tlkf=io{qhV3#I09-oa2=r;v+4v;RfO%ND(PVjn;u_7V-f=(!I$;ppTBqvsBS zCQoAK=p9s4Args#%d3aP!xz0Z`WKcOJ(8W1%Uz5|V6HsgIplSdVPShr2T>drVln~g zR8fh<2U8x3k)-q?aJJ4f(TigA|7-!K)TO=2U=&;$&eYy2;L!>@FeH z^Znzco4d3Bw@%3v0iNNF0M=QlLK>%2xEXy%Q~% z$liBB?t~fHUvrsq@MiwaNEjWHQlo!gqi<|)f0Bq>cpr1HdP`U8f`NiXQJRp^zv;N4 z+8K@U1=Lf}zQuU|AF3LlPUx^@|3!+`P2^A6_^WoGPc*#i8i#4{)`R;5C&k1hOE_*b zNyh`*BlBefIVo4v>3O2ofW?|MBT&WX3t~@viEfWFKT?xv(0<+G`9x`M183uGv^RTX zk?G<0ibF@aIK52U2;;^ucwbR&ci%U2BnJPDy~O8(5WHK`P1=`-3e^eV{;l=gccq-` zJ%?O1mE%&CQ)Om0FZ&NYK6wRGrUaZtyhzjYXd!7CGjAp~>4#Xx34kXHOO((PkD|aS zJQPX?pk3zhz3Um7P$7ZD`$QxpB;<+_1_o*?gkG&s>-_mDx?eKrb&CTO?rk5YQk~1z z>z4O2Za~aB`ylYK1=F)+nS2!I?H`}Pkd!Mx%NsId|1r8|gn8-=Fc4sWf)_Gb{JEBk z`(M$NhII(zq?n%0=}or{utgo8Y80~C`hwz7T;eVa`{G;3}<8eH*zB!KtbV? z#^;(WdGw*fph0RV?w1>}D_<;`>RIWVaK{G{Gz;g_9*>9C!p8+lymR3?_Mkb+F<-+R zrj#ODa|FqI8{yHn%sfqJQ)U%J%b(bdLyMxF>AOZp$$<2Tf{rekF^$3w3vWnEkutEz@$gCw}^{d{DJ^p?1u@>8hIqme8}9DDNPe z4EHa9`?K18;LaD>3Bp-43TZ1#UdM|w=U|p%1QeAx8bwDCNY>jX;w&a4i7j95BtU?~LVuI*cPt9&0ei%Cb|sEb*= zDl>E<7ByTicm+>8G@~ctoceVdtu>B(^6D+zK$qKVcvqa&8jMARcRTNb#do%TH*+P< ze1u=pLra5UxTt8ssh#;F+84?v^180!+j?EXf(4OWa_bvs*cz($@GH3;j@WjR8bP)= zLBY=BQri+S@W@|BJ0ZU){^(-Dkc%DIqT1kE@&?t)uF&J#xn!!knDXA-V)j!YIA+ zm0a}N^$#{r9ff%V(YO5^F9g2%&hG z$)cro!nsPpYUh&goL~CXu{psp<$h`S_|$Bmdw;>J%DOIaNv5SW?!t5Pz@=1&`5f%c zPwJkgm)CH8S8;DP^`A_kRYrsYd;p+z`p-D(^gqcLYSz3RG#8wh$e94F8`9fe$(scI z+XUK23ZYAtjqg;s@fW7>f%qNKPuzxN6tsEqFBG|z;H+lQd-I9DS65guU^>Pqzo{+0 zsgWmWaA+LQPd(~XyteeGIFT_9Dte(-7dfG!f|W&=8;{f$p{*$lpFyzcH5maf${(zr zK~}Bv(ld3hVd{Wq=s_fR=8qMG;*dkns?~LxG_jcRZ27a=xcVo_P67fw-Xd=Mm})z6{dE?U%5@azEnNM|Tla2_TIkBUVjJ z7hI-Zvbm}F=QYu0p9wFk&uu-+jaV;}On;!)z|m{lo&<~1|Q9B~Y-qls$)f=j*#E%{5)A-d1+Ok!D( zPMeuUeybPlwMf?$cpqO`w;H!!Ec%XVQ&k<-k+xt7(@Jlp+`D{tZ{98BejP%H-!`Ko zyEV4IFgmohMn3!)?j*6!mMd*qlIy+eYf7Ep@oYwWYuo20(@irFn`@%i=Xbu;=e_)@ z>+PgxjK|UbhyBYkLbltM#dyXjUaqtTaLR_Fz=6+%$NPq5)6pX;JayF<;9Wsrp9h?< zsv}a;vVyNDU3!S!PX1ykMvrXQTzS71!ai`Ov}W3@b>zgE)*{y?-qinK%D}8P9HWu) zLYGF7Ke{>)!_O;b=#o>fv9lR*V~w$xgX>g{6WfV1!2ywjDv*d(N8$Z!u=41 zxTR>OBxU)W`$K;d?*B_~ES78wtl{iD+e7fX8|QeQP#-=|^}Aww?x|xwUEKElyEn(D zhw;XBUL%!##qIPwP>MhJvu6I%)y-$B+u)9zLC$5^D{RGD(~6qS|f0SHrSZ+ zL?dO?*9e&@o5+WGBLMySmGqHm`F_m3!q8(YvEgx*ZSzrZKh|fO=*gzoO^IIF64<@t~V$RUtq%w$j40;%KQ?A*^q zTT7~PnB{8=E<)!y$QQDY1lPYBhYA-y{|>IzDPgUs;4!quSH)WLZ9GkF#h+6Nw)0;a z{2=WqnUEP&e+db43%Q~fN(UnVTz&5tz(f%^T%;>ojIg}{?IT4)@F~)jxG6U`erG%$ zejxDaCotqo&P)>*Kg2qZtu{#3xVz9)@;CSQnI4&$@VD?=8?*?bVZnaIzDj(1gD-SR zUu%_I1FV}-`a1fOr9cBSM-3eog_p@jgbDg4jZ>PcZzaSl>TIHnacap9%W44jblat- z27zKP<^iW{`piuiSKkZEx^6y5tM*haJ`)9*+@fwCD75E!?|r_3;9q4`6%^$Yp9<8D znR5MYsJjK6n&PK#q5|2-;lVu1dZx@_YY-pY`h6yB+_~%=pg$~y-5f{(WBivg*RC-| zeohAqe^ugmxt2Q)od32Nr>!m1qKN77`el@WZa$mI9a&@<%djsc*cpy@LdUO&$m0{a zzu1!D_cqC)U?&sxvg$BnY=qucnKPdaB^7uaoxMrI;clp`?psp}OqqJM&l#2A*{<%( zAz#W+I5WlNlFuQ^}c|UC0g4&mHDlSO<`T* zgS}^{XVs9?#Jqrzx@py0mgpJ>NSJx~uhXuiveQ7>uwUu~GR|b_or&Z_{Rl!o%FlX6 zbk@2`7<9Bt*qZc=9SVj9JrfeNl6E5ywzs)>_^Tp?W9iTgw7<*-Q~B=mtGjRQ@!=-Y z586&@&J|Z_ShBY?b`Gd-dz_9lmJgRtP|KI8zQd{&K^Rnpdx|P=ZkJWZspqKcKbS*1 zPvtaI=h9p*b22qdk342iR%G39PNvn!jX)+{#9m{1XT|TeW<-PBIMr}-IezIGL?>JB zg<6kR-7@Y(2%CV0VrIjay;4)$UgdW9v8QJ_yw;dLD60gLOkho0pHB$~|0kza%$J14IOtqIW#-=&^mUd03+F3)-e(M@!vKHn{WQN{!gsD63rpD$S`k zY3?MKEE9bKamcy74O0G{0WJEvIIUUMzN}V;86RGLfD%&4yk%8YP2{o5j%;UzV5kb5 z!p5yMWQxJ7Vt>egJL$ugHSA^C-=x?t)|U_YGp;Xi8ttaRX>2^TT#)dF38>|^riKbr#=MY z;c_C&t&iAoK|7ALN~ckUPhDXrOmtK4*yc5Br=z*yT;ISTLn7Ky@~I&uQ=Sv;)f2lQ zUNqLt1k@gMC5Z**5Ky9 zE^q#GX+489j~aQYNyPXse1kEhGI4Hth>f~(lA8ILE6ss#v2(IO&8)LJ^tCX5Z6x>EK_4mh-a*{Oqipm4M(mMYEd{gJl* zu=1g^y#so7YtA{}c9StP*QPEN+{Bx!21~9wH+NIlghX6rIppyh@8^C#zl#SH_Edf;z6aFb=7Z8Q&-cl-6`S+u!%&HLY6LB+zQ)HY-q0TLoY9La%mCyYCvg+ z6%<op3D+D}HESArdIVYzXb;+P z7v19cB2j7Cs1bUvuMzm&J)YZe&rNC*gRgg~mMYWIb*B)K?)kc>Vt0fWrN^IcZ{m;~ zwu${TqyEDu46LfJcrF!v({e3r9VwivYVki+1?RGh7FuW~J7$RHt<>36P#b>Zvqjff zykyZ1+{}l8SPh1!n+b6GH02zA`Egn}u^T;X0-V+PO0@K|azoxMK4KO$$U6c)BkL-e zcasnY>BfeJKB&17ZyM>rqN z40M$6*^8vHbaoSokNuRj{*YJpcH%x_8EpeMd``2C7n5 zQR*->+d+<$Y-@hlq&jbj$fb@R!K(_|r>hq#OKUAnCjHL}uoHxz{?5zQ1rFW2j@{jP zR)3C3n?z}MZ=ziqTf>gxCUw5Jom`*6TU8*Yomv&CTt%yHq%CKORnH*$=9{QLV0~ja#M+JxV$r)Y8pLIydu} zG8!8D8*YiG>E+E5r*H0V#q~0D%^^+wW;Nh*;%0{ss#0CJtcZ&tnd&%ox2rW^2g^14 zugKoPA?!G_O@VuKe&kNKQ|TMQFPo|Q*2=Cy&xo#4>gBe|{>z$IviwZU<=m&D-+o`u z*`jntN0N@7c@yYn4rztGo{$x|U85>vf;d|GM%bmdh+-6%PA_uh*EZEz%g^aD#g#DV z?lr2rRB;DY2>fi$Z6?=k)}vtcCz*fDzLIbe8_gO&#*`xvstrEwS(9tDB`fmkPkP*h zP~|WLt?VhZcQIv3aDTOl=Sr2|yW|6#IN&@3<-fABvVd}50ip|6S6tw46(s3OK`OA{ z=P^7yB4%jwM4)CM5Qp?tCyVE{Dt!b-9lAnt#Uu+c!JNEvi(Y|NX=O{x5O|&cHM#z7 zZ5;-$GiCKwF>ss=2?%UJb%L=X*05{Wf}@96IIBJ;jVtxTuu&q#Usp<$O1PM(N|e6w zQ4vyo$L~u!^+(uG`@NE@oW8~>Pu zSny`YCdG*}xeMfNC3e2o8Yj$}|9q1Ezpj@RRiG8B#YU0mPC_PQe7 zTVPo_0~tw2V95hDVn5jk_ni4msx+Z?ALy{q2}}k5u!QrB{x|$9Dx%A9Uqc4@cZHm> zuW2aFGZyIW-newtM0~q~j6Jh(DB?D5!m#JSV;ZE~SM1V6qWyWZUKEl-Ux*V#o*pSV zgAQh>;_7`vyrS|V31+E>o?(%sR){c7VKCM8U+4pW5W~^^+ocbVD#QIfWm7e~GoH{t z905V5jJ3rWKe(%I@h6+qPA8b-En?(s>Q#%*C(Qb=e~^&FwW2EX?PV?_vEo-0vBrPS zM4P`UzxIXo?4wN9TK40_J6hK6%hL~`DT{W0)}GqJe^;hk${Tr%bLr_?Ue7J2{6|gO zvE@zWbgTf+mZK?+Nq&B9!G{)3JO83jX2q(|rfFzj=lHlX`~i527Httn)XNG%?X*~D z4hjltT;GwFAd876PA%4Xod;px^<&|Iv_VT%RVTQ10CmW#-_GB%G7YAttgGQ|sGXk6 z?OX!3kdD-OO$qaIYDLN-j#SZI?%Hp9bGU4HZEdoD$1maNEbn8r)Gzffu2ErU?-SjU zZGVMvi#fn}mRv|o5PGWn?|P+Qp@rDJU+j0fl8s|DKNFDNYMhKSd{|gW5>Ob|UX|)P zj}TgxOWWf4ia)8iluB8&xSsOuDI%Vs&k$>FPgLUd)jccksQXvA2_=Ss8ZD+T!VWZp zE1IQhK>j~CIQU&t(*;U=LGcNcX3Wjq0|ZKbetvMx1KFz~d4B$}H+~|STP9B#O4$QI z)d1VN0Wc|{pr8N(8rYj?YH2||${YO4w`d@;$>wIZSFc2C|8V2ry}S+m2e2gpU0eXS z_mMu~dwnO!cYq5FSa_fWr+VkzH^-n>1uBHpLL_ypJIT4OyToF{78odxSzT=A#LF=h zC&1Xfb6+y*alnd~gL&^sB$ zC%Hc@*6jj1vEqzZ&OAvk@iZNeaZUX z4qzin?o?~f4MrAYW{YcrL_;m3DEzo1-VuXRuUW6D@p@cz=v`s0eoY==1cp=-*|VamKF^^fE%HRn&RO-J6n z8SoZCYdAsgnZt~(n*-1|oxr;SotNU{lb0-V)M?4c(Z}@x*75M@2~uq$jp9|zynLc* z%=psV2nwhj@qcC%s3->5iT2PJ*-m$Z5;V|A1n^S4*^Mo{1)(Xjn<@Rr&%W!K4=PWy zLeH7Ena|hDtL)D+Z3DYaN7+hyIZBKY@8p6-Mew&t@eJ%O;CnuK!Ud@6sC0HA^zz{V z0*rG1&v;A|qOde;)@V!BmrLrlX>yc|=Uo*y z3+CyJJ*)N|zGu=*bOk0Tc~^S6`+i?mw278hIO_W*n6dPL$U34ZQC6kiV5=sfQtO`b zC+JM2TtsQF)S`2UBTnl*{)BB^SWVP+fl{jYt_64Q)~x7P!g0b8iXFBi$x}YI=N3_L zOzpx8w5yv3A#R8NDA`7;ijl7SKSs6z(Q4z|}s_Hv}AgY~~}=ptY&5f?uG2 zxWE9z?kpu*vYNUgusr{dMq=jW1$~^ztLDWzE{A5&dM}#;Tsxq%*Vfj89`?cEVMlO` z&hUHk01^bEpvU0oeTS{#>dipVvsLTu`? zccmN2O;orL5x&cz;ouD;vHj^`x>`ph2Uf{4T#O)csc-v0g^92CvWiD1!p{}T+=A1Hw`>AlFgJ)5spPSw|GiR_&dug-5(|B*JHQ><&DB@=fbwN6tH*g1(b} z4_HD8mjtS9o0(!_?=RHa{gx&u>z z2ydkz63zaZxQCZDmL^GBCC+kKSN}!4Txz_s$GQV!Br%@ErK+_4v|;a&{qLkRm0!py zo$e)jQ-4tmNlM{K+cWD8F7*%7CRcY8-Lj5N z$Tz8Cih1*$E+L>n-70z>jnaGuF25vJA*)P(5&2|MpjGv7VXFIFUdr-V8vn2h@>S4-F>wH?PlvOdw!}L z%aPRm{e8~qzi?RKJ^?wU7=?n}as z1I4xjajK$%73`2e5GQ}i3i_V|r2;|kNf6QLK09LrRv%%83J3(k3Z`8^nF1^cEo?wG z0{Deou&D$O*0nXGNjm^G0=37`-;PfY9(q2P zpUZR_{(x-Iz`y|LJ^tNT{P5%Y`uNVd4Zmx@4)}&uQQf$uJPRIw?9l8=-vuLWNPPPt<*=++R+nOyl+tS_@MV<0RSascwW-|2e!S(73~^j=u=#$ksNiT}lMK?O%$rJkq$c%k zgMFuLZt@on?rSinW7Nhkv0uOJ4hiK|^+!ZUci0$(ML@g*6Tpq{rsE>%0m>Nxt(9xF zW)lm%YHNW#A?`Oj7}ttuC!=HJ-FLEbm*zVI|4gcQ(MUgZ7tU{;1bk8;AzHI&8&JX- zpA`Til-$KO&qC7LzcC8$ok(N}1Vm}`%8hYmu}uNf5?NkI%t$Vs2|Lk&ti$s+I+72Z z!;d}PJiIRcO*EmrA9%Y*SIA~{GQ623c{9m3=TJ;O<1w1?a&mSDsyqrf_ZS}Wvvv`y ztS@-E2Ym}PzTkzW;}$O-m1MSH;SvxoiY_RRHunBK*caF-;JuCMy2n%;f#ZW8Dk2H1 zGE=`XHj&-_a6z4dlnw*f6C>sD&9zvLHC7HS)XAlnv{HGU8LH6*yas@x%}v6ZeoKy>NVI@D9D_-lj5gt#v+; zE$bKw%m|vhvVRU$2&Q(LhKA>CvlEZXAIvGV*RuDXUil-&MZ4771o``m|`9``? zuBeVKj$D&jg$oXJ<&MDUsUYaj?{c8d#Y3xZK)+JetO;l-qoV>_T`eUHxZh?hT-ukw z_zZSSphgqif51ACt<>(aC>7E-4#TBaMpjFenrotSCZNZ?weXN?&>8_dqh_PMSN>Sj z)(wsRgUz&SIDdMso`Xe0DRf1cR8c&I5}<{hLe}FFF}?^7+@|mp&40B-?|T?(fXsjL zs(?UejS0)W`ymxfT5)cLLw2YrC?`P zEeelkvx>`qA8ObO9Kc`&lgayX(S)u1C4}Zw>;mD2Ipx@LE|iKaQ+g~(R%LLbijWAflMro=Zj=~TWr zW0o|2#pc?$i?P*Prl=?$u(@Ssknh`5OBgzQRS-HZcU7=E&{$u~W^SB81usPtoJmSG zrO`1GkBO8vP5llfCNUzD7#`oi#rh_g_0PWz%ly>tIU-p?{=q}%e^xIq3Mk#uc>3L` z$#A`k2N<3_%)sHZ+ZiuKxSJwM79%!v5Szw`)-A4EhWBXjhQK%vyOd zm-BfbNtJID?XBB;1pu^p?Of%~>BA%V1nj51apV~IV_;%z1A$=5?Cg`~Dlx_moAhqK zCZpQlx7KO2M{_2|2KFaJghF+OhtTH}QuhNf((h+rUJybKayXl$KtzM~04UbYFT#}# z5dn7}1!gb{3Weq)c)!6J&TY{JS6f>gO-&INE*r4C- z!p@^WFeAU(NXIxJf+YK`9G)DMkOmXn`v@FM2xQY1v#w|^eKz<`g0xKB$jhRup#koo zjCN;mKe3?LAt(QkE}1zkeK>X_dgeC$>b7trjE$J@aAHUxfj7?;1t$V08jETvWbk8P z)4oPl?!1gTnXP-kwBHOSnNGd1As1{R+OsTyVh`Y3P#0k1v@(c`Ggz`)$hK^PQ#)YR z_*?4hleRAXh2Nh-XReh2>hZU(Y9?O&|ZZcnV&s+PB4cnC;o}gIcB1NC? zVAJs4D(UnJvq0}OHTAzB_@?61hT@5jkZ1Q1Om=%-9828WHWHyD@kLu)a)8R=>+fvN z@lZS`wIfsxT*jhNfNILn8`osy{?ap3&%uH1_UFI{u(bKzDL#J#Y9Z#uN87!{3*re| zl&)i%)4?0gs^sqj^(k&#mV-?+%ls%WOz`Y2Y@$y)1i#NNHg<*+i{QRMSN8Z=SJs}A z{-URAj|)k~BWPJXs7c29MYIBrWfOrMS9FoUyLwjLhh-u}TLjBI47TNu)}|o#c!Eg9ht4_ZjE9b^@S2j<^QYoLifAr1oH`K2w8MB z3Z%>Sb$RPF#~AP9bZ zR|fl|e_9l?=gmk;aA1^o<}q~)hN1up0x-)!unnACFpX=QTBhvwjqvz?pDmU6#l zlJPz-oM(rw^y6E71CYp}eP6|MT8&V#&I2VbfRw={Yc`E%XT+}6gf;>^zi0O94usoj zu^M;A1!z!Xwo+BI3lyG4bD^uXgl9Eo_F25W3Zim;8?GS(x)CM)7-hjQ7z7y)1X+(tI>cxxLvoYC{I}Wvyh8%N%I#&9?2pTd zDHnU?rXc-P^?G7mue|%Ty@d}W6|@`1&54yep2EiPrk7;>UxsojvTPe+*75FVoJTD_ zg*~LAOl?con~nyC!CyAf4mOpEBlc?1)BPL1g&Ummwq>Pd*#Y9*O4MJ1t`^p{zT~6N zL!_rD94{@YaCn17$*pMe?L1i{9N>P(f=Vk8ezrP~M2I~-9Sv_f6$H~hP=PsJ8?)}i zAh?vWmgD3q+257 zW@!0Ap2U3x7F4dDCD^9?Nrh2S>1pw|ELHRB@!#!1YeKO$*qgH=)VJ!Qr4q@Gf5Ajd zN<*&t;02d_7INN-&*GAXUHiC=lx)=9<`%z!+77C0!Ft;%^dJVfgqF};(GYOB%}U8u zu7LE6+u|K>u0iGbXbw89RCzfUyJ6c#p?4j9jAkcDQ&L1 zv=jpA6~?)R8RU6J=3Vry=FyaqApDo)>)J0V}1DKM2`)#t~x7HU00?nri>34&b3_V4@SW1p7F^gadw zO0K9kg=01@1K3E0a18}7{o!^YvKo$qjSKlRx`FjYb)|;IVRKxEd?`&++N&Yqh8(#%}FGTosLTc z3q20YHhY_*X$$wqZs`0jUA#Ln_6$#ik<3vodK-bVEo(6Vk7Qr95k^%xd(cV*u5VPnbmm#_495o2fl8!5{%TdyEoynQg}}Etxi;&9 zFJvDb*Asd6{;8{N1)!!W6YQnVC3b-D4W5l4_z9AhPz&MepO?P-b-rQH<=AUu#_nyh zTFK|Tk71G$v#;ft0w4jnQfjHNxLhxYWO^ zm$B`ug?tyUNieME6~Rz)ec>2MX)2-yr(%?;{^C$LCH{AimV;e271qnJa5nURP8g9p z)PXK%53Wk(#fsfssZpnIiP~sCaepJZ-jm%Ea;;G36FYr~!z!};b03qTa>a3zhXm`{h&ybxFa<=ceq~^%OZ@#C()m_B z@I#icE#CMUT&*@NFhS%%&st}ZZDBBt*}3b-rTk7zzG(f!n-;?o#-QBfd-)NLCo6Uo zJEifm9De`OkUF4E>9r~PZYEKc{-`CiJ1qnx^N#VU4hH}R(G@42l}VhySQ;w zN>uc{3=_sS{#xxh3Jfz?lEuP14*|c4D8?Z}_AtX9K!XT2*5s*0C=_KU2siqMa_XI@ z`tCE_+?=&x!d1j5O`iyMM_?d{_va z*?~e&>q1+g$_kz}Dc3B<+8TfMm)g=j6&VXd9cGWlrK$#s~_LlUP6*UFmC*q*)G`jd_Gm?1A5mN&1c zDDGg3D&eqGS2>X~n8iSUEtLLXsXA$Gg&XackvMz%ER%+zPbZIH8DrG?JJyCU%@wsXxg zc_C&+Jk)ax`)h5P_4t(nDs99+cQ4u4(e{qY$=8MEM_(ijabE3Z7cZtNQozDzD3sd= zARyeO)GE0k{hmg$yf0q-aaB2mHft+;h8Dirm4VCUfk3WfYhv&8@>K(4WEH<)4K(U7uzk69=yj6kau%SfV{fd{`&W)r)HGb5nhr6**YHat~V? zDOoeN(r})nx%g}4R?t$LCHWOzzZTEj=iEDzJIqhd5sqpuZy6HlWaiahhowiF6r04j zab{r~?D8!U`>Ec)a!xO?sPV`8!)WAsA7xC(vd4#G+0*T6qg;C=RFR$#xovGwOkiH< zh9MjwSvD69yK>;!JM}|copyktOie&MY{gwmrw8V*DxW-8T+WzkB*|dpw^>~!CnM;Y zCCTsH?g_O(F_SuJpeVGfP_5*&<=7_*@czTbeww?ue0DNR!21VOLE!9D3L~r5DwPtD z+0jMK7AIVT`TUpT4D}yt6YJ>}Qyyi4jQH(8&z^pkv|LwHi0dK>J`u=r;g58LQtJph za13Q_CG#Z`kd__ZJSdKEYC?)`$7)ujke`mxH+Yps`Ahf|WoaXK{Tk-B3ZB7AU*~sU z2QGClaH)F^6T;v z#j_~$bJ1u)7*qIbR|;butf%Ak;*{_!Ww^)>C3hOZ#fOevJ}etN%#kGbCh$Y; zNC&obIh?b^h}j|quphEfc3m4kQOza>AM$8uAt$=O(lHdutQ#s3#(1$8&!tA9wwSSTl4((I3jp38Ua|N7bs zk}KauhA_X--CW|g;!cFbNEbNh;0i@o^L-CA#wzRzo-{&f%t=1B)zkvtd=fv3*b9j4 zjD5vmRcfnk)Sa#<1VM5$&ofRH_cH-qj=6--3H32C&D8W1e5pwaAlJh+9w^H+D;yMl!gJlqQ&UTCQrERM8nTBHjL?YUsE>Xv_jNmqy;m#K|*~K zwA*^;qNL?QJ8T}B{H~8d_0>(F)URFv8il@E6Iv3xextP^{^XlN)D#B;(htZ+kg0>9 z?j9$np^cec=c||bzDU5@Qd;dbENWIa=Qnv5=t5j79{0kSN(8E$Dt^8M$pF|-NgYwmn;2_GS;YE5rQWnUYYlqiQ3lVH6vaCjvODI$1hV1KdmDUw(y zM3@DL7Qq zjwQ?8GLaFKO1O%e36Lq?n=|k#sU+Tf6oY4r4=+q<{*o-3p~-W=JsV^w1rPwA%TG?} zLMzht!kF}HzU>yP_7F>R{-(XWJZBd=X9xY2A9NZVgUfvnt*ug(_^DWQuvNC=_t)?W zUMIeITN~3eb&M544n;qjn!c1}NZ+wUD|&m{xX29XYcVKI9tACiGgp}&`sZzRt*#^T zI$4LjB!H~|9fX7Ywt*%7BB(@$wu%2{Y4cw%{sxkSF$#HjctjgJF%%0PGrCr?b@LJR zmSy|nR-Bw>@fwLOk{tz$w$@3BubOkKS>3QiA+@D?k5!Q?eJ#T{j?wPuDH;>IO{+OxNUly2 zp!$bigEbzA)2(Nl@8hh?l&)X0%}ZtrVBiy|`-m{w?YAq&4R5{4}7AQOTFIW$ShI!4AKJBTu^&dYB zAD@8P_a{c29C!RG8H2l|UCAcnzM}?d^ZIlN-z8d&#zy^DOi^~-pC>{dgZf0`%mA`Q zB1eQeu4Ae{kvhU-382};%6g1Lb)Lq@I0ErG{pvkeo2F52*QU^xwJ(ZXsWb@{F0>z( zk4DBaYo#<#E8euSmiq51K5D>Cv<2*&VYRESicN(+6TzxngnHdeFTuz?AGST#d0$m! zf}rqAlU{cP`)V7yI=i<4#1kY(R+@xjWmFAbqiX?}so^uJ z$aaDJ0AnJBcO%0q1~(xrGu10v=(6g`xiT;z_)=X-;n=ZFPV=+JXK(C_CJeZRt&AI? zyAlk<>A%9DR&+PSGh)zaXf1Fjp?XBu&Fwms=Uc^|e^EUpFWRL?##-YR_UG!pFK= zj0v%{LovtDs!ax9TZRHS4Xwerp8;ZxKi?XjG&>p0%k7{n1AG+}Lw*xDgZC~qUH(AJ zVr69&0q*D3mLachv2r(s);8Yj8uOB-qsFkz`-V(t&&DVGk}0c=gO&aS24EZn+IYiy zB!4|f9r6I%9B|Ey;hASxTORA5`kK^A$}hUrq-#wKuZvhQmu-81jt-!ud2A-AC#R$s zIyqsDoYPvAo1B^_HxH@Vfs_Oz3ljMb*q>CR-;g0;_f`=H`IluN#krWY#^rU)3)3kU zlyGbj_fV%QuUrAsgrn6S4DrbN!jz_7Lu0!_OA+;=QlEQ1Q7T5*wa^I+=}kK#5VX9H zqMo9s?_Ok(`loitm`=UpP=)hoj{18qpB82-b?GKjxDVQfDnL%7Mr*S@0B!-?@bRG3 z+1wo3%=^w%S5`ZP#bCf2kzFuFSC~xMo6Uo}`nnUu=K-^_chiw06>25~1Tm#BCf$NS zF|mgOW1#`iMJfcXrvJx^FYgcC9X=c#KQS*q9=X~5cKAWWzV=8Kl>CTkOh4y-@@g~_*Ngv znw1EaLtb(Sd^7HlnqJFxNQ%$H-5=v;D19J3!oDqPyW_saeO}Z5czJpRr(%283=9+9 zI+Ke2yQ$P3a4e61a9i8Ag`Pct?7mz5BRtPX(BW^#o0D;?_8J)}ir*)q0p)e}D0z%L z9}FChqs9BxcF2mYbgz}2K3v17pi;ZS7K_@3#L+7)Oe!r~OA)_*X;dT2F<)s9c`a%E zxq#@UR0Z}nHu;6?&)oI>ZcGFWgdh=rVfpZ&AI8PJp2`ExTpyB4LH87TGW~4$W{o3a zldu--=Hm5R(Ev)U?VThgOScJpsp49v1jfXoCgFi<0{k|mBR%dJ=bF{vJ#F13jevAYcXx>r(%mJ}!qD9z-Q6{mfOPkJ z=lgsAYcWe)z!391_ug~P-k*Iv)NYJa0T8jkd4?li4(duM6q?yFNSxL&`KLQ-<0)w9 zVVe)m;zg_rdBVt;pF!w)%%{c#7k=5K`eJ>0nj*;%88Vq^)AuEqs1P{SU6~QUEnlvr zPm~!)OkmQk^tRg+A%MoodcO@POEZ8DQCTDX=6!R9d+98sc$3a!KrcN05wmZvhC#rJ z@&2GmcTcxJFSI)}iAJ3%ZZy9?UuUMa7?@&WhVglUPy(n+zgSs?fz1)EO!b|Po@VABIdVP&2 z{tQlqi67V;>`fN~-Bcw=(E)mQV^Xh}qdS5FA{rL{v2-TMpQMir$2|3Kic}dgt8XkHu&NrI zB4oa2Yd=G{C#Txtbr=C-f%wwcj*S(NoeR_!-}tr57ZhE3yS zYJM|$Y2Xb@37y6oFARf?vq1+-v594P2R&X!@^Sg;<_N@mV@siZ4#Hr2Sh;F-Ltvw> zDCg{m=MgUbv)Tct(v{Fgz;@uw?_4AQkC&X|!N$hm2tccKAnO z-#-ywi(=DkPwd%xj(X{$LpI&Xvryq~5EhT^MR*Kre|zS^R?><+Ct}zDV2YW18H$Pwubj zms6u9GVP`YPMu_>G$C=jn`Hk_dm*zJ-VRCxqh2C=E)SrH zNolk=pYI#DHU&3%y%zLX%K@LyRXdY-)4hQ;$<^_NN1~`KYElKce&?D3ZT=mrZt8Xs zJ}cBXp@5YF5di^V$zvzuerfbU=UqyMh+ihKE%0lFCp)HbXnT4IW{t-68%Hn9oD2-0 zfy$Vc?Z6Ui6_{H2fT0!W;60D1=(vtZkn{S*gdXi8G1WOx<$$$cxfzoXfKXNz_lbS< z%kDj6roK*0)fE-!feACXO`7jd2LA!#2?%HE3olPrq9r3dB-049C^Wf65yV$ z&{*@oTJEjcwa7O!J@J`Nvaz}1nMyyFU^kK@PL?ux{}v@7tT*}QH!t=6Qnlh) z#83mlKA^JzP1cu+k3S_vz~x+{g}La^j~`l6$~0+PW3M}HhG4^8p~J%1O-<3ab?Pi& z<9k=5udoEzJ-jUOQu*X_FEgCU@TM$d`Y9rYenL$-B=n+%ABjUF{Cla^OaolC;_oSE}|Yvuq<9wIA8kTSs9u}CIJ$6*X|w5RYbVmd@ipjj#k&1+pHFk9zsg>`%06;% zYuo(3!JQXh*+&0qapvqInv4m_+J)I$AROCg^@U8Ti+>Nv{%I^C0)H`i%=9j3Au*AO=>{~76v$?NUL}A2ULv#tV7s$_XL}5 zO8w{lbMKuY$xLCVz%E(^C!nwei)2nt4lLa=J3E`M3>FJ4@-Qg^vbXSJ;PUhTAte)* zD%WV4fEZ8Hs+%38Qz$71O}7Cn5`OvQC^wm6$rVmT`^U)>e*A@M zB31S?Q;zdc9*2`B%m58!4ixt1|Jvjy!#vNI7v-$liBsf~+Roe_v0!@{tfmD%5Vc~E zH@776g)?Vc_S~Es}HTY1! zi=iu$yK!So((zHv{-*(Zq6%?WMY91NV>EnfwruHCF9@>8JQ}yVOFyjRRjW;5(v(ZE zj*KUXylP5+Bj>PV9@+H4aY-m~qja3f4Woc>{3zNb-foM=0}x#*jE~( zOO{*Ti^f(-91_(w7`3mvXAP_N=f_uSSjh161|-T(;#W6+EsFga{c?aZ6nXY{tl2D6 zW6@ielrOEM<@3k&M(s4k4aGzU(N5amj_gbeov9)1U$RLnXO0`(G!*Z}?R{uB1;Qxs z8Muc<$FSm2<6Eo4mpY&6lxYgq8CFcabf4obeffh2;WIH}cAd611W#;eP-UXq%$r=U z8}T0~do~l_%U!Zz^*}|L_dLO?sOHe!o+;HY+M&1f=_`@o*ZIai*X7=9f` z3yYMY^!4wtc&vl5&Ny-U0CS#{lYVAWjOBmL>i51kA%8Z3QGfzaQV@xdLH1*c92{jf zZM^L)NC}&`pq?03V!}%yCW1c31v^-yv!bNv&KtP4boz%#!??f-S;E6W4hEY_Q<_dR zI`1lPhR|x-W_SFyATnoO1?;&U53jE!^aao6K-vY&f1yy2<0jW?b?PJUE`-L_YYw<4 z0I9+O0+I?c<1Q{Y#Ki||2**?B=QTmq;-H%Z3M7BL7BI_KWqb|P+I;gPt0WC9(GA`G-zYKu>9yeP_DQRg<>x6gVBs9V+Gs6ze!>Q-_ z`h*-Co`v{c+k%IMPK9=B9`(La1$;txp7TVDh$?X-0GA4@b zDB*KjRF(OIG}Jnas2XvSG)MJWK-8$J@6YOYGuV6$^$2*=S*zGvKGh_Ba^gWkKl`Gy z)8(k6HlZ`lBn5yE`J`k_2A$hZN_eCV{Fhm8aDM@?Gw=+}%;04t3v^xAGdoM{BgO(x zrGEX=mi8IxKC`r64^$&-_(V7@$0W_jB#qJ}?c^H-gIWNq$eFNLe)oVFeHI9@4Ft^` z$_>~v@rju3wHw{=`Q_!}j(6l;nzSs6ZE3$HZ=^^NrICO(YW$JtDAYjQfYfk928@@li%6 zekY^DvQ}SsC!VD6(^3khCPVMyr&y?Ej(py{Vbg2nU53NVv8Cl&>_uI%UYW_mTXzao zpAJu1E*+IEe>K%27l{R3YOBiGu?W03|uCwLgR9aJ&9{1324YPmh@nDuv11LoDpg zld~zf8i1R3T!i<1gI}~V(>W%_1R1&yf>&7*auD4mW_16Gx%zR7OusZGL00bdRil4^ zK^3KDSv0?^h@t^(FGT|`}KNo z17HLL9hdjM_9}GVEGrLWrqaEFH4m+P;G1Zp{V$;rJX4Om!DD?`5DO~6tdac!t3D4(Bj8d@oNyJz|7`<3u=GnUtM#ng=V z3R6pg|E{)21&;(wKLmjQE;Hr&=`vEG31v;h(5_5h9iDe<&70E$$?F;M_L>YhS-MN# zo)^Z}7g?1*bovNb(%#lEXVpH}R3|_hZ4iGD5NG^EPf%i7CB1f+YfYE;{DB|Cm>G#< z;Q4~Vj47QNXS~}R7T)kp`EvRKX61#1oG-;*3pN=HAt6^jWJJ<4V)W}815{EAYKz?j zvou=^jaAah+^=-ESm&B$^#=WrT0em2g+#YjE7`);SVOGDI%48&vf?3og@4g*Cz|v) z5To4;$RQJpC%2AYmUYeNI4^xqcV7qRM@cr#>FblGpv3d!B zaj&ZHngO646q!K(Po6dU0XOYg%?au@08F&UQe=&8Sl;xvnN!s~$LOtnJE~&-0?+B; zfP7L4{>HJ3An8o)28j4c2O`?-3)7Hu$-3tb;;MJ{|IUgvYEu)0rKo8NN(?;xOug1YNh> z1z!J}N$=h83cS`Z8QJLVZ}to{Zt#Xs)z#EU0-Ef&IbabM&RUhQuvRoS_l;cQWfoZH zr`GOSP+p3?G57k-bEK+lmyy;$OA1{)paBBng@emX5Lk0bXTepyTLA$1dCCnGfY-fu z;kjwTKq{vPQA1Y@L#7@~VA5Y>{KTI}REf?jfx5`tAIC}3%6)LMwNI+wxU!|~MDlxC z7oFobscV$gG{dx|3lFq6_VQ`lE^uYC{X8Qd)SSTJB#Q86^7j4Gb|IXiOzA&F!8#fuQ1qc)E?;Y{LiAyvfr9Vbd z*73`P-o{(|&PDAlQhv$rGmReNgy(tjXjDM~gL9q`I@XcGC zpK#<}G3hWb{@oMI5B-K9nY7LXnngXF!rwK+;vp+d>3DK@Yad584D{GEU!@=7FR>d) zE?y^oZ4Fcyp^_J`0D#ClMF z?>}o%u*>(o$}xXNAtA$p@tX5h6+z24b;qWY-v2pQ#l=}w1Gr7%(~X6OU7*ah6>Wck zFb#BIpdg$STH18@1%w{{UqH;%{y^4Uu*kh33=Y_XO)RQmsr_z?UyhV`=h5qMZ5&&; z51GuJ>|yI=?aB)W=O>P-V(jjXSRY;jvk|Wg`N`3Zr~c^XtBb=B5dq&!rX(Er)vLl8 zh7vo_Ejb$dH-&m)?j^4_gIDkR@G>lH(pr7y#Ds{}Xv5e#03i{bLKvbT;iTbJw+=|0 zfVn%y(Sl@5XkgK9Qlqtuz~tuYItRc!pm79&FoTxS{L+#!&{}}&10akwH8l;vss85X zrmuD%_-Rr=gB7@D0t24|+P(((Aee{%bPmk*tgCARq`^R>1SF5Z;bdAxkkI{A)2X%C z4Cm~)AnhwzrxfF+)ohav1hKjK#pwk>RGGZnym^p}{x4+Yic_QKa$bImbzv)K?eA{b zSJ7VugjxYdU&DT>0H5PE+l{w^zgR|(+*gX&YJ}iWcmleENw6>+HXR9r4XE5ENoi*% znEwceiQ>(OmfU8C6>#|dW6QkcQ&rtAY*w-;@uF~+IEF8Jl4xu_Wn4LtSZK}N=u(|u zH_wusK|AKRIbN&}z%T@JPia{q)}KCh!wm=^CMPF{hKJdK4H|qlj0eEN z0<=D0Xr~H7N)hdBY!04v$Rv^9n1<%!Q5JhCd!V4hix;I&I#Ys zqj3dkB?+~jZ$b*s$w^r<~W#9BcG$RbsfuOgD#@lvyHlds}|9ezcmN`?~{Ipa2 zJ3d}&bM+GRpG?{=2^lL2pniy)=VM=#=8gy2k~;)K|F|Obt7%t`t1aU62&ydLVA;-DM zjSTk>xG4>eEFin4RUu<&B&Gs~uF+$O$O|@G!_*mSgf;mrP?PZ4(cuxV?xUovza3k< zuVZhb;l4Wf-=v|Ark#zwDYu)V&woB|9cj%nD%#Lj5%MVjZWKHIbaH{aD`#T~;GQ=< zudlJXqCxu_X6OU3dS5w}73-6f0}sxALM&N;>aS!1r3wrU*hMRRf(oOof-BwD7K@%5 z97nz4vtp0Zy;t4cg<_Q_fZzqBPfQi5@X(2LTeMy4#ozbTF*Xx~u4Xlch^vCmH{kWa zLVN>$PFq&CHeh}n=!V~Q4jr5|OU?uqjf?nyEo$!JS1FrSaVB%D=Q6X53g>g|>`&Xg znXYY@vHvLVY6wQpP|{g8KtRS?fr7(c484MnK`bsAFuDd~IRfr3R*T(_?^oxXzUpK- zZV3b}#ZeVa1L3R7z1Pb=7;;+<%6N-an8G*xiZB;5(0FF<{C{f*G=%m#Pyd$M_XOWB zM$zwf())HYYx_TFVz>UiB&;kIz8#M`brQL9f~9I(+uBO`&;hW-TV;pwLQwokefR*F zoRmp}pcr+~#V_hE37?1?3+n(`%^+74um74%*gIK=1wW6|sQQM7B0=Uw5Jg`6=KHoT zT?s7B4?gH7zZL04Berd}ttc|%K`|rvZu_)7S$5>pb%*z}kj-0#cIU43zGdvw5Jm5| zzl*Q(PbXQH3-31#G5(jjB>dlwfc+AF`J6ub#VrD0{EG@lILx(ydppS+!yVD zW*c_kZjh1@pSnY%hlM1#YpDy}s7E2!-X=XeO@pz!LGlufD2sWE2QU-}5I%u6{k8HK z21%`8+C}NGl=?$iHS-KVo{5l67TF67faJ1r~q%qF|O{-K6=T zt{dhKM(xif9kmx=B3S0X$-{C$`U&${bW zI#Ktyc(y1@Qde?#W%ZBX3UPb$FYvM`NDw^AM?%Vj=RsO$=RDiIbUeHXNA_-heYy{^ zlUvc?O(pMZT;5leyl7m>tXQHiQ-qhbsv*==dDUwO$U43^kr;mar5J7Q9K)2z#3pf` zS@||49QOgXxo&0w-ni#@x2w!505%JmlCbfb=6qXY`|NGwwBjIm!NC=2|hBxqXv?+~mM z&9A?ynU8myR=uvAL@n}AIuOsJvX!ynRPBAnhYkYk)et!;2 z{oT$l680^>cx#aC31b(=s&3lY{}Fyy%fELkd*1R5&w`C2-qMBM?({)Y=TSXLQRbMd zu3WE7#qQnZvaoz6?S`j3oHAqX1!2I(t_gOezMybQt$OMvLzQ~tLuVA_a-?He@_kZh z^q-DGKF=0fX`93z`{E%g3mU02J6=Ai-64DTSlpr%-c`&-l8akCT5ZPnLdWe0Uax+^ z_C3H;0s4B-9fBRANIkC&8!4LYF9lcq)-hxmt(=9hzu<{IHpi@2A4W_yqC^HBXlRr^ zx-nVL_P9=0Y-woJwpTtEM;=sepOGeT3iYarjI&vcq5dXEgR9xehKB_1cgjh1FuKpJ z5m=*2;`L}HQ=d=11)-w#MXx^gkIThfnbf+Iw{+gA3^tEyLSrVd$VjfP|KxVl(meO; z51{X$%}4aJKoj;w!P<0f5uC5UPw-Tz3N&CdI+;Y)73NRaF2JfM-gg2?s&vvLA<7I% zGcB%`uMSA6eAEBBVsP>ND!XD^j>PvB8ZfN_QRmEbZRk>g!fc{_HSwLRST*|%QJf>y6D#GmES$tIQH!-4>&5~?oqBi4J(0=#jc>>(nq0f5T=OT^ zSU!zuEjh$dn8Hgs{eZVgpF2ZYGie@~;KdT3T6S9<25}XD zyhjXd#Xtr{6SofZDEAYr4=`9Byuj0epf@Rd$`{?%pl-(yFQK!-*R% zgLtb^YN!pd64fK_<8*KB{ux z{MrSyK%{B*H)8>Vqogt?F+~_rWc{b(l$x2mzdL&)jc=|$z4kpc532Yk-}6cDa)_8R z;7jeForZ74C0l?+uc`iLyQ1aHfbJr9H4ch@DSu9y`!LX`fB$$6YKT>O&ngnq7LGl{ z&_Vp2!Z%nl_e=I_;nvw3yf2jA#TiVD=uY%ip@MD^p*6-aFzo_`)cmNR<0i_7NMb|K zu>>nC2S}g?F}GmR1+6FMHORZr2sL2GdZmJ=sr~-&Go{k+RUZDR?gP{osAte}WE1V^ zd*PnfKc(?SRwNV%Q|JLN_(IAliu+tUkE*blU|FV-H@W$>XNz;0+ry~9f&VKh$|w05 z>6c6g_$8#3Rd(?!GJJmGq1F%_liE;_E|`*)RWNNy!G(Vh&M)^}e%II_1UKyo-Il+r zXNDW%8eGava;}$OSM1Z5#s+sOgm18jH!qi{Tyb9d59NH%%*N(Hd@t!Git;W-ijKc^ zb~CLp8g*r0?Ct2j5}o9%(+x-M#y2H_Up$mINCwH_7gt&5cP-~e$a1TPb~_er!(|^# zTL=QaDU@3kGwBJQ@W-W2X?Hs0p>+lBbIqrqmZ_(sB3<{Wm-e5lec7i(fd>moqU*2) zVn4Hucf*!!A`wMWorK;ZYQoT-T3>M*MVa+~R{JJWdsCscp#@ua3+A=%nrM7=2|W!= zJ)Y)z!qXhC*CQpx$ETisH<+*WmX$`Et6w{M&~6SW^i5|(Tk-2Gqc-TFo}4#-c}|jU z`}&3Rv5!gINMAf~|0*!!HAlw`r3 zw&z9J&^*76e8!xaX?qGup;=A&ynYAFCv3COVDL}<&rVKs=FF5?!|ZNP%69DdWjk8a z5v9Eh5!R_l1O5R5xyd+Z1cC!3hM-WiY+-EpED(1C@&>_j0DgZ!<1w_j77RmKMj?H0lE0oN)`aW?}HBaOREo!;K0C@|QWywuF!12U54Dy``SA%sOtfx){Z3Dn zr{6A)N=3il3Zk|j1aU?{Y6%$s8_8X|Vp;Q62~h6eu0gHZ7iMI*q>$!`dtckEn6tIh zov}1haT&Jp#s#Omm-k6+jF@$5Zrv~*>0-;BJWzs6A6NVhz#Fy|h0L3}6!-&Wp^N77 zZ+jEp1W`0FPm-_D_!Bu_dhU}2z#uTV5U6wuRT)0<$*qR4q73oaSuvs@Azofxoq=#z z70}5B=_aEhKt`Rgy1Gg>&&hp+<0o~m8@GNzH#LI_(wD&c3UDLGpl$|2o_^CRp6Uk> zoidUsG*~q%JeVh&RL&HpTvTo5&zJ`!w}Bv39adq%IH%SS)%c}OU44BsAV>kvVGp9n zKo1CPAyMyxFd(I+Fb)cbkdSHZ8Boisc=Y<`k1hYuR7Ig+Vl7z4z)}?9TJW93Nh}>vFvxc1FfLoYQO^O2SADwS(^DO2oF-jM z^yPt4A#-OOReSS;N;nBwyF4FnB*fY(r#AC@d+*unvp-DEc~hj(Q~{Z}Dknuc2f*h2 z@R|lop_#h0C_WZ6tzcP!mCeoJM|WN;^TcU(@j!aPzVXoN`ekxrAlUR19^x}BrG3Pb z8J}%N(zDl5&q=n68IsQefA@;#4SMGd-YX;xYLjNi6SD{`32nYdlUGIs&8T#;3fCwHS5~T)|LsPsOu1U_SR*IXqb%(iBgeefefBz1vgg_HH)9Vn{C{V1?lDve= z@4t#@v*Ktb4O~#jHPHkGabp~RHm>`0f-f_ z8fQ9-;T(e?dHWNnBn4D^tTnN~Tuin;M&6f5_UIPI z7y`XjIM5}nO`HTXCKQ`5mVk;LiIgve!MI`;`^s*=?&WM4ush$y`Q)klvD*NkB7}Ak z6BPiJghq4bZPqrk*`mAc0F+)Bq7`hKCs8%=t93cIbuzbNGXL|zSgFhIln_5=mTgyo z?uB`Y+kETF_GH%mE6|u>#=ukutZwGmy_HVSFH>J{plqLc1Z$VkqkDB#l&Ri79QE0GAvaXrSOS@$RzCMaWPEu1K(tQsciA|7l66T$RJ_S zZykbd*yrcYe;&fyRPITJ;g#s$9!BOQnt;s%By`ciI z$jNbGP=6tXaQi{z_5~1ez9@`q)b+g?)Cb#F5a3(;`H8?%lR-XSb~Z(2V;M-~!*9Em zEY)iX#rdYg(i@15&kLR&m=A#EVNCnl@4nX$mf}@w1CmM1M$*?o3XKWSE2uIA#shXr z7YMH41A_&iy9Kj(Y~SPWbQ#5%6+CJ3kAI^ce*k+ZcUqou(J?fP6}ZIQIC**95pZN` zyVQ!i!Hx&^Qy{ihE_wx;&ExPC7+rmEUO0%nTH8+%a4NW-x{FamZc9h4#`+<30x@gJ zH2y^v0^58iMrTJJq%~3zrK@5=6g4$?0>v)1NmkaZpXYT-%4>8?yc!ft^2A&ql0Gf+ zHZAf-ITGN_r$xf1M&6`ixIp9rhIYxhwEjQ;p5N2alMrj`^;_2FV|dt3aC1A`Hlm^^ z)a}-KMgl`7olKYcCfNC&rNI(f!JJnT{puPjI3+qc`-Rr@s-NjxKGi*ge$1GHBn$^V zB+v$3=L1OrNk>IxHzl%8g>Z zo;H=a&Y?~GL$mABE;K$*PUa2TmyAO7XhPA( zQdup=4=MgM&>!9PMh=mwe75@#3H>q}`ApL_wCel9lR_Ye$Gv$`I&B%VYM|(}113y> zk`7!P)C0Ie20gTfxID7F8}*t0{xZY*x+H zcP_51d~q`R9ueWf&BId&&gKx+xs`=4HZBP@)1L_=_zmB@S9^xo6I_W%z##i9ag6(A zJVaoNTWxJl?Uh`*4Jy|h>U6vDo3HV6iFw9#z}kT{&PlS^Q(+@I-#^G7?J}Ke-^4h7 z`0@90bmR-)M|3G=88F8rB5GRtvAesk(;z&o?srVt>UPPs7r|X?b21FzP^*szeuGb^ zZTEiHejxvY@@Y>W%vUZL^-T?ZNsWK(&mn>#xz}|jR5@+zpnA7j`(2ugp^di25K(jO zoWu&(UvADovI$^gCnpZKK(ZlhJ(0cI0=VW11)z0qXl>23fGHh{vlLF;YiMsDebx5# z0F%7J_OI2umembdfU_n*v&;&pQME%4@;BY&GKm&gg{A9KOYv`(zwUW{@1EsOZmaJc zx}i25Vdgj+ciVgy$8n#^|Lo(9&+Y9VjLUm}=Jz;ylOu9V2{x8m9AXyj z`-NqT6`sn$=M{l39b3x@j?cy0&#VTb@0TB9KVZ)-@tIeR$9i8XC#ly`eYSI(wnmC} zFW`XV@Fi7D3qNwPrD3kJRDhS%{@!iuc^1C;z=32t>{3;e!1MtJ5l9D!A^l{2Bfjz8 zJQ63TOAOS5-}SPO6q@mBH{Z&q=vUj` zcB}u$m36yWker-6=OeaOtpC99bRzQjA=CHPBDDL9Dw0J0%8(cJ{yS2rk!&-l+&8m} z+T2^-NJSRmOU5cWop8^p(|Kqw&63%d|6GW@QC1EXX_^3OXTpLh(4?cj_n8=ZwXdos zQ9VqH2Fdhyl@G+nUxRMpwbzwNzs1pypZN(&ohntjt^6D8%Sug}`5DEI+c?m7k!Kd| zArA7ZK4@L)s9ow6Ou^Qdc596U#IzCn`S!&4gUVZ&5?ppSzM$CO@W_uitR1~}uhE{Q zIA#)=X@51fdAI!$<|l;p*#7eq8X7($xRy)sL{OQ;G5akeAQT-t`M~{Q5U)F4vMbKp ztz&0Ns&87R&r_Cul}Z|=hJ^gBn@LkCBj(PiJup#5(ilxj%jUiO(@gAQR7xw~TU|}S zFo}|@8dqW^fnzC3IYKA_aMkL!N zH*wR?N};7G&DBSS9q{wkMo=?-WiWkt zGi*5OmMh(%Bgt8P=2lB`e-e|{mtscCeJmH3VUoQtnw?l3d(C^h*jRh(8cAKw9Ad>7 zZKo}hHlyla`MN~sNHa0eM0+kVaQ|Ox)lDn?EcJ#~wP1f8%gps;5<>uCGAX|^-CnQs z&li5`PxA1SApMflv0=gd9ms!$8RBA=8nOYr9M*~ef|9oi(=AskvJE9VbVQbQi!mhy z#VyRhB72mh${=YYD=|X=`8OVveUiY)1Kc3>(f@&4$#<+SHZPj zce9>0wcRy&1K2vihaELYh09v*vHMz!141Ni)mOHI!KtVsBK_~mlp`<8aKF5@9}Ghe znEs5F--KcM(iD{{07XyhM0)=CS~c`(&pAjHBl%ZC){HiV*{oo*QUeof0pe9e{T#bq z#QO+J#bMW*%irH;MY=QGRzl)q0bv)`x(h^i@?-gGfm3*Sga0!57u!@C`TsfiU`WqW z{WgUbt#zemz6Y7_uCR}=hD^6`;YswB4`ddeVHk&tcu`AmsDDn~ull?-`8c8WmF&0l z$OM{dt19MP4I)Hz0?h`%xd0UnOkXRd)iwxB z2O4Ew_hWLvx460SfCGj*0ObW=LeYiujl~UnK|?&@Gv+(;>gFL=0f21Y4TA5Lr#E`K zvlq`x6bi#;Il>FnDjf++t9Sv=gd7&F!!4^%vuR({-4*PLyl3t_9-N3oe@WW)Fi>7; z!Z`@e=%7zoQl6WazQQ1}eTo6MthZ=Pe&j zSi1l_!Eoe@qvMl@Xr-V5x_Z@v-CMl-!?4(m@4v&wB9e`VATQYBv&vl_dEW_{j|t4bFw9X zAP2SvTjBe4Y^Q&773LhHrd2|B7A<f;l4qK#WJgfZUs4(!dEC$m@vtig!(QD(yKM8h;dz6gV)$#d%8FM0gCHa2}$08 zmqFz$VRJW2R3YR4cx&4J?s!^UBgis_b~<^|=GI!T*l8Qv><035)V97s_9P3!M5RWd znD8oRwkJloPz$I_T&TzAMeJl(BVrX5vivp((}>Zu8ejSWN4mmmXP@}tKnvOr zOth)}PH9)epQ)(V#jMyIXXNvs$@z%1pF!KF!ZzivYGqXx(UMnq`5)dWmyEWU3>b67 z?9fgU2h_9k!lTi5_aN)D#r9I6LWLAu9^Fy@D z^M-ixte2ZAh0$tH4X-wJsAp3g$RSZq711uQ|MoYEXQ7i6WUZq)(ua9#DfMY=a^l^T z$O~Jk^ex0!;%$|HJM+^J%Ik@5-vG7OB(p*rOC6!G8fmmibgwrKio3V}DW2OQD)WOI z4}x=6?Mojmhn~+rSow7xvRqaB{_F`7*&*NR`)k!ivwNgU-Pn3Zif7~W&u^XG5Q%1G zgUs2rwKe%kR0=x0gwCG7f9V3npUFDGZ-}9YsWn*)TV`#yoN^jvN}IJC#1i*rmz70< zzci2#OJCqHdgf4Z;d5pve31D>4>{hnU?jAGBwLnP{E+~;D&NL2%6k7Ts^M1d%lP`F zfz~o8E_3{Fxw*MDw6$ThVbD5<2_FGF9LPJt=?k`*Y&3*je zI!Yw<7k;JV#ZSzTN(B-i4LPu+Z2?og_dg@D4VYpBjB-)HB>sd)`u?eY5)!q=6A34h z&GU}n@~u-H^(&3O2DHA&ZA7>8MZtHLXDZ0V!n}uUpDRDk4{Qcx7k2$o+czzbbmOn` z(TdBLnhp{JvHWlSXO82twGHJ93|}ruhTiN+n|r{MOx0-zmFeQ=nrGt3IS-HVND%EMJBr4U} zUZ^RSQ`9#zbq#8`(Zs{s(bUm0w$SvSOqhPHHlwDYt8e1slTZ`EHRCoj_uEG4NVOk> z|CLmwWjpzb)D^cXeNNW<=7ohiCV!iwQC(kw!r6KQD6GAdyAoRFBH_9~^O1r7U$ulF z^D3GC+GBw0ZV$h-6xFDmf}icz-H6v`+l7kpO{-bFw z$2+A}7n;!?VDsT)<{dXXS^FX1MnwLgJNdB%o?TO=YU>R)havoqYf$?erAvN{JnzR- z!z6JT{}Z6rY0yzSDqZX?y}Un^nc>jB!mF=cn2B)uq>`$V=sM*lJ0Cr<*dc|)j=P99 zVvc$9!zxvIsk$1SKpg;<2|h%-Qf#46Z~R3IU{vj~ct-D^QTwrgsghod#U@6Up2aF% zA~hmmc)F=budo}fTG7n2y5NeTB?fiN)OdTGF1g;_`;_M0&nO2Kh!B*x>EJCilmA=z zqvEc?g7VbJ&~?1~8l-t2lItOKS~GqUdFlR2U$v)yQh}6zfxgQZ^ol?%lyi;EtR7Wf z?zD31!t(41uS@Z+_i7Yxl40&2N6J9xk90|9H-;cV2!`uuS_6vd1pJKJaNzKVZ|^Rous>r^B`lyZeZ? z7ZJl22ayZ*p<&%`fe&kfP6W8AVAW<+6eE^vgIq;4t*$E_$q2i#s&JVblr(cH-FExb+`9bd zmb4A3B-B#V<-W-65Xl{34JIR!}We}+F-If zDBR{#+NECVC0O^bN)`J`!8gPUx)bzZCmJz+N{RH=cBbD;ci%;yBjdeGd4yV@0&VWf z=i;uL#?5=Fb~WuU&mJu5o!U*`vi7aLaMrp$psAb;(7;*71DVS)&-Cgf3YI~CY}}UX zMK-p-;$JVETMdqOjaVlX2UL4wIR9W~s>o=Z!-e87Y^m+Jz9_=+XT1(MAF)7HZ5SpY zi9V|wr}BZQItp7!e6}Tf6NmhZHj8xh#J=FdFLrXCXYLz|U206a&VIRDc5mHqQ=}&< zr(8e9C-EsFW^X0kANLx43{%8wp^2<}Z%5wruvetM_@lLKK9+Np%ayc^R-q--d2v-c zuD@<__jp2<_2g#MFLTuA&_29!gfhvp-E)i8PjE9$}Ig<1xG)Cmw3 zV8Un6LpTlgyAHlT4i?ER72@um1mo3{0mic9VLRVAR_}FmAd$9E)PJ`NxEg$Jhr}X4 z0a|ar4iAQ|JaBsS)L>xmePE@(0f1YAFdj&Q=LJJXz`i*>HCXh3%~pTiDeI1S^=8D4 z*YEM3RH_RS3lY*XsnUcUyg{!J)1g?g!u7)nQl zQ7;U~0z!0tbpHACek$v8R_l0fX7Ml*VN8%5P1Ogveh0+ebaL&={zPID>(>KV4VALB ztdg$elD2H}3Qs%8lY4$2pZ2Y8I^OMm3p-e%3#GWgYYBHi8_uQ4@ynnY(_XOS>^<+{ zI3$Da>PLyw7daa?BH_EKx(^4S=@f|%Bj_`jd@v%A{KelOI?wHAvb>Y`IjX>{Nw8|O zVnOw^-t^zx8TPw08H~+~zj%7g{ceV3?F#&AjmzFM9fE_7T_nyp>YW!BZp9tHAyKzyEd#I`pEDwLHB658kL{uK+KsDCis> zuw9S;0r}R_1(VmUiu@VdeKC~Y$KS~|JCMm#ZB;qHZBX}TeCMYBPzdoZdX>F!z%5o- zd;EF1$iBGRZo-GPnNRtExi*zOqIU?6hDGd?z~s+&Q;Rn(6Rr5WrVsxza(V9gE-=NP z{xz%)32v)4*BqUw?G*ysQR(MVAQ7l*d$57Ey{hIBGu&_x(*frIAVDIN^nFuc*DS=& zCk!}3gmwi-H^6WXHo-_2@=65ls#Dm98X6iFsYs&&Z2kwd&h3`6CB3=$Y z#uHy%5u=N(1-jdimF;fuw9vWBla;NHL%f{#NS7NGKLoo@=X|~}|178_pqnb7OPEHo zw{K&@a=fPkKmV`j>UrJb>B~O& zv}S(YC|B}1EM;pX3-0glw8c^n*oGCNv)@)Z=St$p)Zh}XI*c0~s%f8YS|`~J-Fn;1 zY%oreu4W!p&ZC`N86M95{(WC3E|mJ$>ziuB5Qg*JU)uI}Qegh=+0u&qm%3QT+#`NJ zA^L0)K8CqsN}B!b$?T}jh^hbZUf~>f+cPPJ$IerGMZLzvD^}j|Bi6%@!MX`^WC836 zIDz&qG>x@qolsPCl%{rg%H0Sx!iYq|5hAQ$;!jidfQ2-_Y^}aLt*d)5(DeUEIt!qv z-ZqMZ2uLeZ(j}crgCHQ?v4Av4H%O;QgS51yuyl7zNq4h!hk&&7_wN6jSsZkbQTE>Z zK5@?P43(SvLPp(^ST+8a8$tQUxYkDj5hAP_zLMYErwi5QI8xE%rryqhWflk!m1M>R z7LCBnUW@le2KfW-vaiVqVk@v+3x($LPm~{Bg)_!?(V27ORa|kLCo_*$&nN_kL~hQJ z?mF_1w{V1gWBl&M7#{z9qPctcbo=iz1We@ z(UA8J5A%wPBh;C;dj8C~q=PO3Zs7WFa10caG2v3LA_V^^3OENL?*iKXF_zxF#ne`a`T(3~6 z`^Y|DlE|VFPEd>-Kix9k36*BoF2T2*k>w%5zaob?w%a7Ix~}wBWSfZ%BW%okeKcUS zLVYgG!2iB$I6$L}SlO2G^T&~@ZxJr_pjXo#Xke<3(wV_-EE2Dyu4^n(sI!VM)v`%s zzcJKW{`?S)4o_2o7qwzl6aN zL2OBm-l8IG`YnCH(9Nk3pOHZhUnVauE-D~g-d&%81_%ofbm-z+0M68|L4t}q;r%HH z8U%kAo@E0Tz4q<*Jm7wWjR4daTt6h1z1rB?C@1be00axD zFoML^=nR<<3p4E{9()NkSDb-%2@L9h6ZRDe$xrQ4C%9lkzjeX$+E%UE&5?}DXj-0l zp02i*czT*RVVXEEfw;#Mk>D%>a(V`F`V4WZ?nHdpmJb@2@ji_OibDLpm10@^IB(V1RS0 zW@*(l-1E+uW|nX|gV^w5It$9u;?m-xk!F@F7Pe7|=1>^QULp-^&{V>$big17+;ZTo z2TkKBhx{p6IDlgrhSiyv1ozmPN)QEsXfdESKhwq1WynYA)kyYsLL`kyFs?>$icBI$Fi}F(x;^P=3BU~h6WjXlgh6G+yXN_kqlyg z&nQF3uq+ixh3G8gue(LEze=)aorr?_H@g8v@ul7@V}dMD^t=T_jC_%MOT{02qJ`?~ z!;|*Px~axWxg%xY!*fETe%hhkFPBDZ=n*oY2fu#5*o~rV5MoX8wY&_Ytqw(VW_5r` zSKA+h*}?bK2wR7j+}|VWmg;B6EWdQl9L&fSM>|NW|Nh2o5yr?jTb+L8ei7WotoGF) zwaUY>b9-l&C8OEuZ}p&puV?m%(un5uxR>d}LprsHckwTrm4TD4w;wBg=tpr@7*98< zO?{oR2b9>k-H&ABcgj;*_>4}fypA=YYnwIhM?t2DO$4VLNrbC=G#1!yA9%4^+!{Lu zI*jw4R`Vx2dhnpW{g^z`xd|;mnw?oYCDzr&H}$cT`F;Vb^4|GyongngGHXVvpHAR? z8a&+37!j{VP%cl@@@9N>>%c|7Nn<0g7^2^xyS>_M34IG5L;qvwVi}|+j(h`l^uK-~)Y*Fd zPKYDbY*(M;YHDtVZ*~n0SLq_W(CAnPbAJ8|0Q&#bVJPnl`*%u{Tmk|DeI{=#QXNVO z9q!)^qBT721cIyc%S%svF|Jws1QC0`SZ{)OUB~th)S@7qB|Sa8d&804p#wGB^}hA1 z@&)?F^AfJ{&n{CoWL`fdP4grj8x@TTB^@heJt}2QE5(f3#9ZsdE+tcjNx%&_U?Z6SRFOMghisk3NQdi7(9i5sVP=A?^%!--w~~3nT-q@ z^qV>eFL~#%&x?H)Fbkr3;qj0E4}k;sBpAdsI&PqV%vEyAgj*mQ>16hA7HDc~1E|Yl zjbd1CvF|bZasGliOqn;xi?+8iY#%+PphB~u@dNVVcVFfOstCWs-C6A92Wq@X$*?}6 z{fXELzpyD&y&#`-;$A_883Hp)f|*?$;`3hhuVs_A{6FKWnNU?;zIInz3@>aNq#-ty1G4z&R_I->bIPhL5K^8&tW;=Bs0m^IM;#q{ zdlZXciK!Hzv$2JNU5YYWQtQ_3$cQ4CZ1LSL1CW%ne@HXd@3>BGrD2EU0T@?((3&$s znlgv<`&mw#1aC5);tCc2$f`NJrWO@PEoQ-zrWREvEjE4@LS%LVQi3yV43CiOmaZSo z>Fj(%+hk_d*l*`c51nV$enYz=u|oP?BEqoZt&8b2HBxo;?HnAlN3O5YcgzQMlNlXP zB>f}QzK$v-@rmec-+3pF#cmYV-sAj^fuLkos?gOKAclnq6N+O17Ro@kcPYU6_TRd=EDV@ch5?^YgKl6{V@jn_LF;Y~@gR zD@YqJv+S`$Hu8gi!+%7Y_DLC&3IpCce3ME>Mh35%>sNf!z+Gi;e_VGP)MeCmSz8Xr zV1f?Md^F=?QTh6qFjDe*DqlFL>;PVf18~d49@mLG=@KFB`$;b0hm&YbnEkJipXUhJ|3F;8L%xK zUr%x=Y*qRr__=d*iwKf$Ke})Yi7XQuYwEr(<9lkGmXXyt>xO*Ybx#%LV)21H-GV_< zD311(LR`4pSnQu1=4l0Jb&`Jul`Gb<*=(O3idV?q-u}M~V}Eyd^}qlWfFKs@-@{e=D7thIx_B9!F;{12cS&RLzXS7^x5C0B0C?Nl z+6r6rmH8xWS^gK^n~Pk>i>$%>T6hx;Z%F>Z)3?q*9QVZdxF~h(-*4l%VNYcYLMN3O z0@pojUQl{t&<<3dMIVQ~`7tKFkG*6RvRokRCoeJklL@vk+!N!O=r1%y$qk*Cg zj^i;ilVqS3E1wH0=g%4Z$4KKPC;Df~eq+1_t>W#U3}ZsMw^x97*lxf2tP7 z^qb5zyRn1L0EFBEagUA7x2-MH)9aRcTEd7Pcn~ouwi?SOLn(U|{a(y7bdKtegrQBm zOx~U-+(x!7AKm6k1iPQ0hh-rIqAT-|nnm!06J)(5$TcYn6sKBl5gI;7GH+DO-T zZ@eZoFJpIf$@}G9Hqw(~sxQRbb3O0EBg=i7fU-lkQ^=bNR)fH80k!l*+5p$&Y?;2P z<08wak#7<5J=9+m`<$DqZM*y9wj{?$!yGPK8-FG^gUmq=dwkD|B|=^M_zZi(F7 zZ-eg~cV!sP>F{oF(eGtCkYs|gzpr*+2d!ZT#e!cIE6ENQM>h1(H}oo)Sufq(ivK7U z|LJG=vo|xBHa<67T-h#V?iW4cub+P3DscOT_|}W-G8b~`TzquFk)c7f_oY&i=rAxA zJ7iojsFLlZ#g{Nk$#l&S$5|rVh-ouTV6DQzuB-v;r_1Vh=i-&a3Ad)qi6WZWH_zql zT5*I|4eK3ok5;bY?`&Z_lGUQEs%CyDH}TXI0-a;8{G&quEgdF)rbF@RFLff9XT;3? zFpurRfSJGe#&ythYWRRN>Hb<|BJa&|-kYiHHw%StCh|BZ3g0Boyi%|}e+>zDN$Yoc zi5e6Yg{|aSGwF8QuIc;fpkD4Yf6!GJlmQ(9sPo_rha=oT_VmNUgYC!>zQoBl+nbx4 zInZA9MpImM#@>v^XRv|g68zc#>Xws{v)Lf{L7wTB) zvHAIa5LXDkat*Fr!NKu*k3jfCcJKle6j7x7ov}1hK$1mEg!d`D#hf}8V1g%SXL|uj z3rvqFal<$OgbMWPpw>S+vOCsuAR9Jl_f3Zv#|^6cRfG`|U;zwvUsu0My)_u{azObC zBmnmI?7dNB8$kIzPv8nk>;V@Hm|AsT27SB4z}vKM7r$riQo`t|oFlQW;>{bQcgRbgAPED7RY8&Ln-mk^ z)w8M{csv)mSMuI|n`!j;aIe2v0p?X;{XVk4U!Ub?MvZ08zh8ljl+*Gdz?`1)wkmAC&rRN#gwd! znuNc&UaEwl)`72iGMS1th#ykhBnu$o#l?e2P$x_ACzDMiZma$ebuf<@7he~iy8dcg%lBflVyUnV!9R9MA zX-Ei1=~iurK64{^D?tLm8GD7HV!>BpBBz_IZ)|27e<2o>Czf8gr2w5pFG(OS!DrG< zGuS#cj+*ukGR{*hQ#W;_k!oerg%y+;Xmq)_w*oo;%{uA$4NCNtC>@h=VmXA2&6IV- zUO*i)`#ASNV9jBU+9mwUIeLsKu9SoSarsm;OtmplT$vh z-C!$-6t{ok89#RLWbuLa$z85GiYgNlV`K+*LN<0}wqkmRVtO`PWHxqs2R8V_VWi*h z9D9&d_lVBB$=(P%Om7G`(>!reTU*2>-4FUegzge_vCmGJJfPS{R=*rVhM;Y&ZQV=f z7vWzh+N!;+oY|N5L2C-~ik9ldGPe4pip2Ly9q3*`-{?_ta)mSk^O>n8yF>k2T#kEcNd3H`Vh69zTc)=GxWgGCFFs~W zOolr9(dtpkkPU%ZW~)lC1y5xpT~Bb2SpT+UhjE)pe5v@bvxT;DePG!Jcp5O-$ChF| z&Lxgs6)g@l;X7;$-bPO!xTFRnGCci?c%?TmMdCtwwZzSvs>wiVe08*R_i+43bJTjJ zzkc$s?oI4enSIPJrL&;58LmjM%!fzf0@omv1X6L~iKb`@JJ~S+YC8*yXM$_ zhDQ{MT+=_oUApduJkOuOpNH3HVEd?hKG40QG~ial5)o?k1}~sBow)J~AGrfE4^yYl zilOc&w~*fsVl-qbDoeNj4ps08lg%#JTq(R_dMW%C-#MB8`VQ;lb4#*Afl^N_Eulbl zN*`xqT<MVx=3hsbKLN8lf?Cm|szJ|3VvnbC*WD=$565#f2P*iK$vIS@AmD zlE_(aB(`r=&Nx5THUwytsu~;lMs5A44#)0P_+Ez2xaTgLXUDJGuWfR`gNWrx!$j}N z75oxC%_Z$RPGNe{>PPt3K3<6)E1m{{dige8nyd)LKffmgnCGd3w+uhh1*rxlh>=Ya z97*zEm_i9=dxn;g6DqZaNXCGinZ%lT_kzU>mq#2eAq%R^iLBL@_?ZDFP)E}SndHJr zp0VQIzd(VS+VB0l{+5fEnHuEP$4q#f%zh7Dd0VlJ=^Cz?IH?2)OGROtN9#KBT;|UV3%nkyvF{r9inJ3 zpL*Y}VU_-)-)?m@e&muqMo~H)*_pv?kce zv~~I2C_V1cJruX!F#lKX!uLq<*x~eJ?~wL?5$&t&59{pzX6oB|3?8-&PBr~5H1E#y zMS8dtNZmTA5G^NbWy_ld60g0N-~Is}K)eVErUa&~rjDQ=oo~;WR@$@wX4)6r?Jx%A zn@KEGW1x=?b@{a-ill!|F165lmTuTio?qisuFkaS#2?$NY5Y=RK&{9tc`;> zvzX?hMmZnrGVYGX^d2^w!rSR`ANAISokV6>=s%Ojp9_iRSZT!X> zr)oBMIg`a+9?JNa z`uEKSswI|8;RiZgq+{b;_=Aqmo&sRm!@PH^K79@m>5-~?g_5|-r>xNgn=(_|nSJ!w zK361+E)HN4;le`FQE|jOO#DsXE?YBcJ+Qs(se5uQ;8J;A&axy!5`QjQc5R>Vp!299 zqSd0;v~;%RQSbGT9@CO<3&TT4R`Rs`n27r0^`I~lZkRi%%OPIQctJ0Crnw6pvEJsZ zGJ;DWiA`L7@RMp%6__Q<(`~T2;b3CDnn({ivGs2DO-Arin4onK(jO{Com}o z%?CNE;sWV(;EI`^-U`aQP-4jCj1^(OzrE0o{8ARLh=Id+nmpx?71HJG;_?DJ1g0qD z%eqJ4W8Lg~-37(Tezl0=(_nC8Z4iG{GnVU;L7Q@a7~WBnZN;blja8;VXw>ybse9KU zo2zBM0GqjGX?2}#b-n9bfz0k2GR7}uh3DNa533HL<&oOCC0{FoUGktsPb$B3E-NOy zV@&vbz++$f%=+#NI;sGKe0O>e6XQztEiy^39NhMCH{dsuIZY0NyM# z92=$cm)vKpR&dZJ$R=2|QYBu#wt!``xaQ1E>i7-TxbD?cm+RL`&!_Wbyzc%&srbFu zcGkS`(UZLh*usaT-nT!C0p_EN`1J=hyYWj4{$mZ=!S(y)k@$+16)cgbxQAI{7e3T< z{cYt(#NTRYdL4xhObZ{EumcCq(1V`TY9G=iJbmOo^*wDY-Qg3iR{>55)k5dV+w0Nz z`(R!Dm9WjK+V=KddY6P?9xw+JbkfvJLVdWqiT5QNaBcdon&|{w6Tk_@zAy>uWKcu@ zS*VR@^}4F^d-TO7QZT*~2TAefqvXf>;p{^$JZW1%-nj-xU@9}% z4E8olIDLyhX82fyARV*V^yZ&vfs0UrB|@jXRzZ}^=|Vfno@4cP#mARzztV);^Anxf zYd^c(1?_e<9i!$Q9)I3ppCxa-3s%imHvIHy!)HmH?-@^MK^N|A4gEZ2w)Ix&vtFuK$>e48o0LNBoXoO;|cl z*g|OZ&|6jTVJ{?2&1YG8{xJCbZ1(v33C9umXgo9;qKJP@bZPBoF^HWQ+#BLf;yCo% zj%$!vO7I?LGUP3s7Puv{3+azRFl_un3>4y!)=IxTt#`b0-G$;`k`?$I+}KB8cCB8_ z8te$JTw||1K3sVmw_bq<3-8tjd>099U*JLQ*W=cHtEca=g)f^oQFlhln=?sb-_o)2 zc)xN6k9h;;QcySSB8qz~g8~P*=vE_nmsf#!`Q*nz{jxp4Qk?CKp8Y1V|IeohJlZ(Q zH^;o7WPm%#KxKc(UuBQ{9guqfB@JHa|93(JP~3e1_EZ(?6MRb;4-lq-S&7IRY~_JR zHOa^+LebQy0PoaBn`U)l{oQCe{d6Cv*_iMrX-L+x z^wG2bv`uw^IkUKy$a!rKf{q^wc!I!#r_2DKAXdf3`%Q9v)YjhiTDc_$yNwKkd7QM( z+nZjVM_+*ER%faNaZ7*xumFKCm^*-RgWJdtR2=y5$iN zf2Kg^M3=a6IcIzG0~UGb-0V!)^t)O1a5-O3Tkxiq$M;mt3$uRd-yYo23(Sh3DZ!H< zx?DVX>#MqW%O#)fbv+O_+}uccW*QP~`f@g#nZ;S|?za*EI|aE!d{Exam3&eLX?v1t zeOI&j))zo&XmvxnCz2Kb_X7-nfgD+>egE%T7Z|Budom{tf{9**{faPC0y@z3@RglR zf^`-k(!vM^*Y(CJGx_IwrQN=Ybs1az94N~jn?FY=dJ>iuZX}+)r~HQW#u!~iZ+D`A z={r_3Q>a$|0VKqMPu^TezIA-_vlQz5^lZPo!0)yU1}{%s6I`RqOIp6EOcN#hxD55c z!%MC(rO}^UCY_D1>xsqCuVrDj z@9|wex_tz4NQi;W!OSUsdry?Z6?BzyV8-drgJ$3(1V5EPSBIOwxnk)b;~(O+mX)gk zdz<@9*Ug5JA%#g3fS)AcwFC4(dA2051gxs8>;NGPaKR-Divw_u>HFQgz_*m300*6d z7JNzo-T5w*HVo%p0}pD8Xq$E^D2)|Z2xwNdb4Y?a`s3*vT@O>uoj;b(hV>YO&B_!1 zFFJ|}aAg7zc)}NVfPv}MD6a`NXwpyhX5Y!|MXCZUr zgXZ>;mGG_FhtGN4O4gOaS4f}w9}ey=+Vsd`N3EB7(`%$FEQx~YS(DmKO<&0rR-Hfp zluDOcY`oR*8v@7x@J1gK6Vn|<1`I|scK|#lLnZm!`2{dac&apk$#a*EeQQseeOo_x z_^cr-SLhx;_3dI!nX%z%y*h1gmnRI?=?opJvt`c(wQW&Yr6yCXbweNjJPpO=PVBgo+c;*s3J zLv&G5{7WBv5c;V|lN%4238kgg%Fzl!6|Yci8rUc!sa6xd7~baFUN!+B3n}1^C4j zF*xue%VpkI^7DMpp?USG^~k}c@|U|RRf#<}Ir10;1VunYZKi9o*rB+N9feQ^mhZMa zRx$Phxt+v%MhQhmXTRK2*VJP&k`jG zO=2;BS_rvEpfbZyWTA|mu4j^H)#6HV96Hk!Ye-PlFw7b|!6EpT-1EEFQR8g&bKkRJ zUO@L;o!bo$C==-Z#L1-~(M6q;LMsNNL7J|YK*MEg*@uQ^soRGUZAxG}(J=Ezh1Qv9 z8N+H8eK0W95aOVD|FrJPQqX&X{}!f{i?;1IsN|4V;inCUE7aLE#|GHEP$*cB`TP@e ztEf{X|C_3MNtcrh-+VMm&)F<9`ut5f9mho`jf|_d?=o7!waf!SfMtMep1i%-)CTnR zKo!+IYZ~;mFE6XgGFJUiv7HC6LU!4wT1Eqv-{@Fs4NNa*nYnBWh;x3(VFlUryimy_ zjwzkf)#Gfes#?;UQv5abSo&aZ2kvcJQ3>J9UMKPE{nX$4`{;5Jax6omZv_Rrz|2Wa zEnc_PDZ#k%J!RI<1LAqz#OVY$C_g171CE#nGehtju-Br&4Fh=m-h~C?rKKf!T^uP{ zxfH6X^zH%=to3%A6lrnS7HX}5`CN%&+|_>*e)?8dR<8LE0>cRa^R%3?4Cd&vo&BV^ zf7at-{rcx4K;e&reE+xab(F<-GfEN~U>DFieb8BH-wSwWaw;v2owE1w1~O5?xDRF`O|M=wye;rpS@ zGarxJb_(iHcjbT1=}cO9#Mp(>F=bjYMQaX^?!5h>&s>TaRvDNyte)Atq<`Kai)|PZ z&leu9t>0CIr^U?M*f6vCca3IfWZ3ax>1~B@&+*~7G)j+8;s7>~QR}i;EZ$a1@%i38 z1=k~=oqJFHvS$R~Byc>ZOu!}hO~pGQU1}6po>G!X%1%AztoQ*b&YPx~qZLgpP3Ohe zvT}^JrX$H9?Vt$e3XK<(rnQ*XrY~%zPn}f$s>&D@K9iFO>7i!9dupKV=x#c(nV*#U z;|CWX|M+G?*l!+T{`t*XnE*9jdHu{vzIUHawtsZ#oc*0?=@P`CMo;)A>0*SujX`bk z%jpGAF?w+IR`i$%$MwgFQr~*Hl|A19TZBnhq>{y(`IYR$smlJ9I@?JL9tQkD^|T7r z;GQQe1oc|mkGlFF0#Zkqa!Pm&jrBeRG*!`!^v`C^J>P0xP;Fy=t>rZ6-q=vvuJwH6 zW|U3q?k=^X8H4V4=v9m0?B;%vOqd&!x7D%;d)0YfJ*;90j4ex#S)EQXk!9azDl})vH8Ys2Eu<-|h6jKKYNQ|$JGAFvpv&QKm# zDejP54;Qwkkz_<3uSc)pd>K%-fFj-;h@!+q=eYZ>rnwI3xPvQ-2Po^E3=+$Q3~pv@ zr%@9R>2FoyTLEN_9HP z@U@Er6EBO#`z?pdqC*Hik`s5tb>thQ4wQS9kjmTvWifG;ZvLoZp5#)&mTD`Nw`9Ks zxo(U*)1SH_$cKc4fafqGOp=ai<3N zWPF#Eeu*W;ro?2ItR#vKd0Dn&BM`upQV{dQH4vW|>QEJ3l(UCe@;=!snlx+NLbXI) zqQz6fsLmpGkg$R@NSQVY%2cu=ic{bgK2+aB6t=B6S!@ ztBy+qxu!Y7i#SC@j`!00=V08tw8jj0Vs7{stb?6gm^PD7r9McMS=? zSmtBblP<2I%<8>>Q&r9^Fs1qlYkY}RH$5yXO$>B zj5jgWl&$<0uL$a-)^f!vQ@$bNc2GuJPy5x*h<~gGpCxc=K%}n@J2cMjqFL`o@k;Fo zA-Qh3*~Rd}%|g({=Cs}x#zlfPkoTsEX`q)`$xH5uRnskOpgC`dS%Hevo^_=~r*1w~ ztga}gxcSy4$XnsHre@zPWQN)JEBN-9YT0&--w&|2wnH%F@g+K3Bfq#tMxXA{i*&q< ze%Be>h05ps_sbl))uP`1xfTb+_j#Kk0Xqe{{2f(PfTSgAqR{o1sP(_M_zFUYnmRirlxy-it_G9&ft| zKRml46C8>~@_O~^4`?x~o7%!`VHRb9r<(Z51D|2jNrHuMgN+QWuspBm`7cxSzL6iA zpG&iMg?JJP%qcwjvw2>*j=jssvKCVGWRaY8|8}nR_1rEtvG0R2UeKLxWV!FPwXOdi zAm8)*JqQ7*Js|esnB2^l^D_JYd>W6&iyjYT2Z#md+LV!lKqB()Xm5`yUD1mYBI$VQ z^)BY>`vjyNqb9T%#=MfRU5X1)_#LN%tCoFAbLY;C{=qq6x+u~Vb}q4&L7t9!`y^vO zqeLS7iJC`5O409svA5StEBAWr+eLFKtR+h;-STVzZDQP~Ega_$dYWm$98N5U!-CvISU|2f@ujlLAE?6Hf?H>~X z>t)C+U_5s~GQM#%X|~iGjv@q}gZK1KFXsxAcx- zlH{CAIiWH>{D>?0sLO5%Z;aiJ^}!<)Fy_Gbgio~L&KmI30=C`^{b}B|6D;{jc+wEK z7Bn<8_<%RY;BK7|-0&ObBDb#+O-}$r`YT{iad2}78F3ZP7M%yKxl4+x8Mc{BR*<&w zmBpi@Exnp99S=U_h^_y~Ft3hjWSVRrDy@lgb@1m|)Yo!(Q&Og5T)cZ7P1w;Q zbUp#gDG!}&obp}&Gc^y4pDOvyG@^7%5VA`-pxmJb zV_h`JyKAi!Kb}|9)()>jBhX%act;gIOSRCjsj@x3KX*2Tml%tqI5&VB+tg;WA^#t7 z#n<&c0$IHe3NLh~mtqHZ8<)!G7Ab#axo@^+mx_K;Gx3)Cnl$BO#S zc1eAl8%uAtAPu`*ul(4`ns>!eyM9sGJ>AF zlm-u-=KSu4k9BLq28n+VbR9pnw6za;q-@HH(a84&2bveAN;@Zwa{N=>9Qr#4EC(x_f+8)`xKgpKYE#Ex zT&Z)3upNe+J1hPGpbRb(ZtXO<%eOz-Q}Ri57HDNH&+y!5Y`Z_SKLcDvPnRX7QjZF? zjJjyeqwMg*B)BF6zgujfQm>1$-;iWyTPi5)z8K)H|Ryd=}`KK=9~nZPJOIXBA*m*=bE^|95+n>3C$D zkGt~sXTl_pgPSMzD_T3}7q9jCc{@f&bk`!~CVw=(8BQ5g(@jmyS7lStO)+OtHueh< zr!FvDpx)fO_%ZA`E!iPji?yHkWZF2&v+(^-)m#SF#bPB!S9wT zX19FUjz^yfkJ04NlN|-o+&k3ZL|t`-AvTylKO*~j8@#F|Lh3LovYX-8Kp1u25i1<@ z^B%ny1uqJR;Rlg=qq*|dvQgx%)DxjL`fkD~$$t|kLbdU==$kUV?g!d>l!xE-t!kx zN5<{va#Viztz>bwHSCBqcK>F^v6BRiT3)*58`*1rZ)7>3@yQX29(+T-*yxRO#YDQN z_JRM3nRHK+f1%vTMtP&}%gA^K!c}yTySG7LG)3r1Kayj{9K|O$dk3A{$BI5Akt4r( z^1I$U+UVQp+9w5CTmyZ7DVrsw`c0a`dN;tM!5%3uZvBK0E(Hps&sZIoqSykGAlUoD z7JD~0UZD3`1%`e;xCi<6>Uaf3*3n-MI1T_NROqDprO;^~4KN>=lQ8rkzW`eFPCs)^ z`VDy6q*L;dbE@I5cJ2CU5cmGxP5aR;XXbQC03p1{y~GMV9BL=0rtXM+Bpj~8?u^Wu zg@9dWopr4;E_Gefr6FuK^lxehKKdQfQZ28>$dnmJ*;dJL<8THo7VJ&*x!NV9Xzlq7h3Dfzev<*|=xD>#nk=7)pdE-eKkT)(w+|Pkj$1ZQ>8EA zcSg1~E($Xy3B*fVa*X#OLx1;TjhIR2Ghe$=rGB*p!k5r4Lrl*uf|M*7rMVpTC#^4} zL%Qq3KBmQx_xa$bWi@?XrbOKnT*Z+sy)dC)j+;WWM|?0ZZa9k0wO7-TKF@<(+gqB) zxwO;@nWii&&I>qI?I?X7{6r!{0V&<%RUn1^_)9hBY8jL@Bc$9N29r!zW{?N1nGivq zMzL|D-Nk~BI0w^h|4GV$dFDIpOHTZY42zB-yk!zp=G$P2}-gl4qlbc@1pTxqJKkt&l#jHy@t{3 z{9EnGZc-}t4v!66iOy4Qz6=!)W9gkr(R3-4ry@dGXMeMP(;yM!To#&8qbD*ZzO^o+ z`3u5REQ#w;Y-~?O<9M~|vt4_a%k^vWuA2L1Kh_CPI$mV5!pNL`dVLdi6&GehB=ncP zlT(~gGxsQzk9{8+tR#*<8(d=_yXk}7aDu!tqb8u}kn`O`C}DMaQuhSoi3|>hyFS7f zyrHhd{hNP0bOv}-h1Lc`aO7c2?RpFB5xy?Vn`So-)t31;XC1pUrI)kGKga;~<-lcN z>oh81$(80vtBg~=aS!M^Q|&hC=Pi0zWJj9t$|-QRdGhs5y=l{_+rQVJ_H#}`MeW&XOyrcvt~$v=gVPP zvELLY8v==PvnCz2`0zw*z+8aIin7~3R{v{LFx2`fS5QX9aFQ$Xznf)*t8s~O zj_A_uc=`|Ky_wn2;6KP;6C_v-hD<+3tM|~e+9IZ_p8jmOs6Kh2(7tB# zWO{4LZ0@l?#H?r;E<~AZ=BuW~xcNGfYL(5k)ENFPmBV`2mJLYS!P*|IBT8^23VH&His>Q;?aXJQZ)(JGJdlzp_BA^Xa7-h&4O0VxSf8M^Pl$9mVo`dxyk{v8<*5d|V`K#?br)6Pf;g0Z48E&d!T@5PiAJ?Tuc8rmu- zdp?Pu(}XZ9_J;ZU%E5&I)DnGZN^rzNVA z70pi_tY^nk$Glm0IW?`ju55kj?}wwNj$B{~^HS20X7QT(?|KY;Z|(m*-d|k(Yo>S% z9JV=vwhV7!2tBS3dLNXx53YDWwx2Fyu*VN?!7c+%+`oyVUVYvi69xQd2yns4XDk0b z+T+6p#p{2&V6gJwhucs=9&iie`u7b!c|}F1wN7OCd=_qn-J8s3tPSIH0YfJEQuqO= z$ua;6s(quhetCiJJqKn00FCH_!R>x6lb35GV4JQkbt%=^a&o`zU3qs;`gl_Q_*TSw zdxxdG>=RM@=Bp+4coWw_TWk6ao?zCPruzz8L4Ar_qu6%eR*Kuvb`ldJltdd0TRR?GZX`JXczTyB2(ua zqmCP2#yDTx%$`FliuCDD`TE0K-K6)4ufG%;5$QX>TtJxA*}5x@^5Jk29JZAy<+h2? zj<#W@X*?$XtSi%Ye&lim9P{H#q+CbI%k-b_T=oOv?pkcGPGyWmh@JWf(REv#65bwX zY%P5zbl<9-Z4~B|C}ctT(0gIxU~3#VX}-L+R882x;}|fzBGeU`@`@?p&`1Oyxig#c z(@z@GcTS;JjYEZMO z`|;R=yYhnw_2F6oc$(F+#{}aIyx{>eH~4TKt}lRp30!~x{{RO5mebQgc?HF>_@njJ z2OnoQ7h5m4#jS1a)((>6A6rv`JMKH{kP(CQ&EJbpBB2N7hdc$f4>yS8ik79TGF9K7 z)xqrFX)c#9)m{96Q4LlUm{^x65o)CmR0%cqPju4_&Q_Pmn|w?ksP_H&Yuh>Ok4*}A zE$a4c+!+U*N|N>Bwn@9GrpJhV1-|1*lE~nCgW_o+pr{6WP$qoDWu}C1LK`Tc-LnvY z$YT(&F&cdZ+CF&5X?eNVCl3(53Sz59cR;G@vP+Ngc(W$hrf^^$>)jnwtw?;egE?0C z9Ku8!(qIJ(LL!r!LHq%PJFNOTHGJ)MRVtswUFx${RlIv|Cl8Z$4kyUeV-3(P!zWJK zMavHovl8$Ko9mpKol~|~9`>&}mlUn}Mo44(SgA!W&_$pRP9;PRu1o8!C6Xn@gxt^R zbjso&|__|o}B`l0L>{12Wi zSq`l9-%@cAgGMUEv|f%}@mlkwag!*h@O?+)_&(NHrM)(w`l6?xvR!T?K_VWynZY2JVXrl`Wa@ zutJQxvbE|NHCV&(dd3;s0hcsc@u^jnf2cV8H_(&S$7dKlr^6yMm>; zWPi?^`1p{)>Lk+DAyZc;p9Zj9+LkKQ3-tJJU{UQ@*ORo@2N8#C2Dko)x6>qD91Mx! zQwQANf@O?q#q0|EwpC+?d#L9nDu>w<`q+~g%Vro#r@@O6yvlhQ%E2cyjOAR{A&>Wd zqisI7cBh}3fQkYh@0pzZ-ldgSnsUxrkN>)(=xGRW38j*#s()2S)4HcpeZBhqp=HaR z;R=SRhGw_*kEyHT#=k1Fc$!IN*o>vHs$PwLTVQS$^5WVu+vaSE8H)}bn;AmY5^lkf zI<@-@jsEUou|2lF!MQg!)3r>l{n=7Z`0|It`($(qs4wd6x-OMWl1o*sT^+sD4JVlk z=?;;@XK$U@7wx{j^2q!d6q6&0YrW@w_XdxeZIAcINYp4t7Gi6enyit-T9VQ&o&U(5fSZ3uNaefSRUGsXZ~%*j3|rc%>_2*L>dVKY=8>q zYO70HaSy8qw6(Pr2u4m%Pj7EHTY()E&h!WS6_5$!=_{WJavoR)*Wtaaed|TyLGM)7 z$MOjZ*Ly5&ro^1L%db{3kWl)wm`_~VAsX7f<{m7solYApyO<+TGJ`nU z87B1?YutVMjcZ?Qjt7VfRhEZ5-fq(7)6eck3kWTif41u1lxq%rp+-%4m(x~he7g9} zzl=$Uv*64yGk9;i&>@CRKgZH`tNQSnNbzy$#}*;`stHV7^bkkgmGEJ+_=e?NcC$FA zdmYb}HJ|8pf5*Ku)qOPEPbo*+{!AC&v?p@&QT9gFA^2RQ^*f2ta*#=7F5V{pbtY;V zAI4e2AA2|KwJfkty|KZp)PdWoFGhw0|Hww@lxpJf5L1*=s5P?tAjr0VdnjGBC$qT^ zC^5(!YG;qWMb)2QODUt6terM>bqfm%U2h5sDs@VDuz)~5aL2b<=kX$q5|)+^wK`o6 zXkzjd?bi`WNT@+r0=cVmiVDK%#MX+K6foFWraV3uh5xj<=2W1IU#`DxpM6}yb^h00 zi|_Dn@`LdA3VHZFZf{Qkn9sV6-YF}Oe%|RJ=0dNFuDhdK6{R%_X{PH?RST;AU$ zuUjKMplLL7JvKgYJ=cGFXWM)HQ|*T1;&_m!q@j(+^c|;<5!XFJEq?BLF@1t;P(tME z66y4^H6rczZa!yrre%kGb9F(VU~^HsU&tI^j_;)MTzu`{o+e(oJ-Yh$uUF(~+Faz_ z#gy7zf974-l@|!;wc(KiSeDtku5y%B`xW0fpWq?h`E6Ubl-3Ku`^|R6xFHIUMJvwD zcn{z?3vf(O!bJVIq(}7zyhjym8qevS=pK(X2R4Zf1<;$;D+KdSA3bO#>2h)yV7

3N0mkMo|D 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

`wg-8Z`V*Ppy4U59=0SYg1@;D|4LkpjjktLs0ij z6cI!!TAmrye585D~^H!wrIuuw!Pmd2;}sQ%Mpb&FOEqM?Js~hTPIQ!Z3F7###PYMmCt@rDk%7CR!S&kSXp6B+qv@y zWq;%kgGE1>@qwKWfQoy2rvZuGxysYN=uQs;xrI)&W!{eQ(=$k|9fX~S>8UZ{RmthF z>j07T#J=-<+IoDt2cgEey1jn|vyUg+`hvpGeY<`s@sqvJgOEk7TdJrQ3ncG*_f%_Q zxRr;SwGH&gb@a_!P|*iM(u`!%0(Y2=H=d&oK!Jz?dTPE{$)KPh2W9GsVx^@DIyC~* zMTOU$^VRZp(FK@A5(XXWRgzV&ZkDT=+DMg{dk!r_4 z>-j*mfk=7rcK1iQ*t>oT1CKT%dTN#8N9bZ%JZmcpBX)U~)D2eD+U3w9CZYbNOGANG zC%-H0QT%VxwwiGo@mz~Z7Rs5hX=Do;&mFM%%EL#p9er}2kJ}^F_WPkAP7nchOD~Z( zS^z4bnL<;91C*w!i?SC1V~$*d=~Nq@MEx%#J?pwkpi2Ai_R48XnGzIH_PvT|T5CRy)2HG-Y2C!(m#~ zqAe@NfqY$Xs>tA0>kX{HfMtA{a!3`-pi0+AvYekb!lBAGp*>0&9P%iF=8$7yR{f!% zt789Z`-3%waMpCLT^%OG{$geYBlYFE(S4wuQE?O>?%Lh0uNcj;L*O*3idfQun6W|S z8NNzqFEjlwbIR&)nq3A<4X#SG{L9a{CHlJs(&i8&SW6G7#h3`hkR84ypU_uw60CT+ zka|MDSY#D=af~e<<~*E~FosV_XrbUYNJ>0@sd=0}rj9eY@Ap6@&o)x*Vjvih(klxBvOoX4U}!VWr&k8!TQgDEKnw4`Hs~Qd zjLXf|1;lwS%Z^Qs!`vNsAM-yxPX*WRI@X@YVY#bX|KsT_gR1=AE-WD-9TEc4B@NP{ zbT@)@9=ee(X^`#)X%PYG25Ao6hwcz5>Be{S`_H^HoG&_q!QRhv$6D7~nlJnvkgf}e zM|!Mb)6)OQ4u2Z~Ke2v`TNN-~(?)YUPwF{5Dl3okp0gTDP-yX{Uk=7V@zJ8q22*zP}( zj(>L@>FG`8$Ufljp$rXMY2Oxx4bn63v8DJN%9m?^LH$m*ji^S+P6egpnTNUUI%{J){{PxPwGl=jqIo-sgQDPcAu9Kw?Mt}hIqE`+LeFb zX~=no-pZX5x|j*m)@Thw7`C}QQl1e99#VEy@SLrGhER%;+Y;tuW6S6|yb5If$fM>^ zv7r<8zSQi|?Mr;o`W)r#Raw6mHehj=w-0>u^nJ+J3YBLq48|+B45~ zDTCHYA6?s}j8}xqmGIURyVFPAxq|gXyWjNpF?!sDeR-nGq7c_OnzBsWXn2p4t21W$ z+jasrE=cCh!bA)u`ic2YNa9Z-6CP{XuD2-gK#&bqG+>^^{f;HlR#V&YuTZLe!%)$U zr_dPncq7P_-SA}ncSrt@<7&m*K{-(c_F}>nw>T8#$Sql4M?CFlu^|?~EkKUb)YR0| z+v5HS#M87HBgn&Gq#wa9=}+G81pMgdx^_oI$wC5~oJk*s3B=R#dHM2oHc_U>6`0dc zp5ZZco!1#MxTuoj6(8db>f|nV*qWyT`o*FdoO#*l5`{Y=5%?3n6Ei%-2<4CiE7SG1KgmCW zD2ES%-AS253uy^YXyW6U1z(8ojApNg%BA`~jH9o;%l?ac$ejM8dKFgoZt5W=3N7%n zb)B4`v1{iv(~aLL>~pE1z{=-6!>S0MBrB3;UnJ3td+_jp7jZo^5OLWRbtX>zcQO<^ zVS88GB9bWn@^zyVZ*Ir26yjfhpI!P^XZsMcJBPKW1TD+OuqR2#lZhP&7s5LtWthK0 z9Ypi9p-w-wNhS{C7nn5#j?X{JrI7y_`Ss+T2NH$1i73prFen<>O%T6nQKH{Y78CE5(A z#lTF2(M-shs>e+~KWf9LL{!ewt@)FVVg^~RJp-dxU}sd%X!5TSO0!^;mT-2J2kRCY zV{w=CP<<1-@-M4a@lMRuInf`8*3t_sni};?YqKEX-1W&U)fseAcUr%>vJnXdZeAuz zSko9+Q1UmEIyciL+kT^alwHG%bcHmkWEA+kjL$l@V)y78#`q+X(n+o6*~WD?6UCdy z3wxHc+q=_%9+2GA(xi7(UEm|0HoNaSEbo^zy8g^ ze8DH1oAv4%)a9JUw^h3(uXA#NqYd1OlPyKow`y-(Xw=?R5+T9aA;|{EE466ro$`|9 zwkmbxwkoq;@9mp4o3+bISXE37sMGLua<2E%O@1*>p|hO4P@HAbrVp~5JWdbDM3-fE zx>$HcogSB$wP8Z48xfZhw8>xsdtZ4Ate!pf3fPvRzql!72huLT#(!6(lEIy|GGV@9 z&Crei5yZ$fnHdPi({Id9NJ)lp4>DdEU!~yK4sDcbt>%#+UK`6W=0ePE!x6YPpS3z~ z2oY*!CLxE;wR!XY?j3YIPf1OMn%9u@HS7|z$w~s@XOSA?rAiXiEDn`L6E7hXNgZTF zJSp!nG5b7;j^(6Ix|q|n4?VxBFt7u*lU>7dpT7I@%PZ+uUcLHgD3suygGC%sC|G() zP0@#lQC{{;+wyFN4Bpf6%I>t#1zGu@iX0L8Cb7MTzIXwrb2RSCeEWhJQ|a|7j!({l{o$cTcWrw3$g zRSx@n-VF|r-4u@EGq`!5i1AaDE)+@{U1{VwUY)gHA9IAeJ?pVv^4CH>lhC~(&3a{Y zJdC*Lc3|Sw*j0ZbdKp8|WZe3zUATqs?ZHonMViHW1y13m{+tLnHwR+Vf0vAx4rCT% zTi9H9-S}t3R2||i%Wy;HwmweRyuF`OH&(3CJsSAl`A z^8THs)$hhuvKRS%t)g#|U-kYF&rxDNW;-uY5$(yxA=&(Z0uuPjpO2#_{f@*AM8ojy z$w^yHQEiV9v$ZpLhfaO3*r((*{MBjcwgy{EMvqE3g+{d2sXNI>xsa#B+~8G~`Odcj zRs|%=^BqML=Eb}j+nIx?FAvz818Imo81D z4-XIL7XUc*8c>-gCRA^4Zvjv3Bn{-g2aEn{Pe&E;h2dO|DmWV(r$akm2+}2&4IFn| z6U<0OR*vA_EUst7A_e!034z)LGXx>LV%K0hSy1YdvkmpM0?YakR07 z{=U!Z2V|gb5K0DUhr3;?6>289|02pcg62cLP<+Gvud|3}i7xSzpaHJDXclfTW7 zpb)R?1ny1vT`I*g_9phdQgw+Av`}Z=iW}O!xkRG}uK|nF2}qSG7dxk@ftzE-nYjDC_JucjMC8jin{p1^F6$JyF)p zHTT6<-yL}W(=9Z$3HpRp> z7EqPwQ^Xlasn#iKQN3Rc6zTTA3Jh~ZH*YuhbQrg!hc+oIsgHXC=}${dpx@eB$}Z0<84`CX7#l@ zn%Yy)b3QE&5;cKd$&yYKyPwjHGLP=Sgc38nH7jA81wJs>O=xz7Ry8J)>?_3dAE`P6 zFo=92>31`-eDU{WFHrlXt(L0rtKL#cH`urIPu&a)dKu->Kb%zmWktX&;A3)QI~cGb^kFagiX zlw?J}RWsd$i?>!1)&;3PV?19N>WlEl^HJpAvu?(ZH6jL#^}LU0&l*}^=31(DAc@>n z`WsTB^mE|_(f6%%q8}=6wr-YkI9b+DmP#Rh&IimFQbA&U+wH;yOI5x^u(RvF2@vG6 zi5UHUO@4d5w7ltz7?!uj$mXg_lb%krQ-dts=(|W!rhgse&^q`>C;EMtN>NWhI>%AF zv0xbTbLG~$U#6A0yVD`hIaADjcEl>Lw5B2ycoMLUp7idHx}2&dt9+EmODIEvFm^iG zo)V`%ilwkT@ZVMwT&+4IfXAB9zghmrLwCPGAnn?7+jH|ZD~y`kB^517>vkoL!=dg) z&+V{EASt=?GA=vk5qVmHYw{AG$hE-Ok6)a=CM|r~^#)%>20?%8TQmMtN zHfYx6*j-zctVKZGJV}J9dD7Nr<19o&>8os=^%K=^6@7A(O?W5aI1>*o$i0SfOu7A1 zrXC0sSxw(C5BmDzf1%B1$Ok%rwt6W;Lvnx(2Hn)rviCLNzl~fMn7(@({f*k&Q6aGY z0K}z^{<*E*Se$Z2{N`ikE9{Hu&E)+&lr)#>cV2{NiIuuY1Rx zzTf>OIhUXyT1K4m28FysV`^$D@ukmE_NVJ%p49j(s{5Lljn+z6>YNUu(*o_>-V`(W ziH!=`um=PJPp!6_RQo&QF2{x^n?sf`aE9?~~E8j2w{!h>Lhd}wTBXzkYC z5)rj0Et^)HOYg6!KkVwoNq;=zZT@*c5k>v1MI>$(bxP*=C!i;_r*Qv~OrslmE5jih zmtkmwM?A&gl$B9&d=G9DliS=>Wm0at-{}Lbf?_664}sY3ly`^8m2mMCN(oMF6%&Vw zu-+ciTa`@A1XN|lXU3B<&Cl}4;6Eo=$j=sN5!3X24k#<)y5QdSnHw*&KYl*BI@m*d zCutqH5M}5UMLk8&xqdW09Dm*?|A(XEl*0!{mNhevZQt=KFMFxg0eSTf8Yy%)Drr4} zc@rgXSo)qabG^gQ2)S}}#`E;hb5vg^Z|3NyuSqymE>y4{i<4Vr%m2QtV3q z_^r!8Qe@H`yqhAXi^uoj>_z0iq?!LNhsN-%`{BEj;icV_PNA7soK`c}?{bBEv10TK zSFD3H^5JsZHYwB*BxAA#`e8Y~VefUWo3VIe*l63==D!&I@CY>Y{>wwkHpH9jnXRQ{ zK#>StyYm~~Q-7%ZA%jBWIcB`Og}r#kB~Ix;sD7pKmOecrq4aO>vpyzEBiQD!Ey3gAtBZ9k%h$DAa zqO{Hyt?NwqKMYm><ciZ+v&Z<0fwS2bI`eYW{x{UQE~khGgmM@#%b7P?ofyAT-Y zMqDO`2!|U9L~jzWU(aov&L6UNXRwd($+6yi;@-kCv_Biwktt^)9@JUlIr_`gA_TTs zw52zs@%Nc#M*o&^ipfU?Pq{E*kuoc2ddk{ba^AvY=v{KuweAR3G@j*g5({l&m7iqe z%(wRQnDH`djtSGCi4u=ucezI|(UGY{uB~pVkL-I64?4CItm>R98M0-M!1~t+xZ? zklZncxK3`rZDeF*u;({%bc_QIr>iFI_ZXs?r1B>pr#i}&2+hI^Ag}i0((3IJHU}Y&+ad1uILX~$cUD#2pljKE7GpwLX!orVlh5M985O+bUnJGf3siUDQ zVq7Tnm6-menpg>mW9jV9q)w1d4IW!m&$s9EiRrI zyDAME$3GitLjOiu=w!znInB(Jsh{@sUl;(EXx_j)k8LhF$#f_T)uvL+RQNU}`fZ zAjR|Qy?D`f$_}gg3q$6UoOETE2rb*LnT#AYrz}Xe^v`r~9Lhjgy;c2MzK(!@%=}vt?J>VC32Xcq18+KX3gtubxSTNjQ2j~w zXXn|x1edDduW2J29|?%12tQdO^Gs4m8ZbF@pc7gH9Kw^zrEybIyD!t+SttiQF`J^~X=u-EFpwf& zCud1B<{E#^SK5^LlE_xnQ}rQh*AxH6V5mjH-~BoqAg;|H4{vJJnV53?>#P$6oiI;R z@azp1@)9_|x^h54KtOov>-!G-`}&?R(O?K(T3UMT4issiKm;zK4QIv7U~JIps50Q2 zbWNVi)8(ycm1+RZo{^g{?(0vMa&a!X5Eak3mJRewQAmuE?>Dsh zec>li0jt?^t*nK51T|uuDy`7Ro+oxH%x+bwbH+cwyR_H(6ZuIuact_UZ6U|r zpR+qyB6@UacO=$wz#HP_ZpIcm23S@=2@23l#6!|Cq~U5pCBNd7K9(G zfj-Y}qqT@M?@5yuk7nu}<7yw?~HA z&-ardyI1?02ac^)XCq%l*ZAkh`k!Pr{Pb=7n&hLVw_q|`ly$~nCM<=_bU-$G_Unl9 zXo0)uu@ug4ZxjI?zeeMf7>q79J2S5wVF7jri~gSlF^@N69=L3CiOd=+hr1h&c8_#1 zYfSIZ7p0i)C)Z`27wUc#MM%^=lh<3M+U1YB+P$e-~v~jvnUt z(hLOZ)rOJ$vBJ7i{H=CT<=v1_CP@)*OSC)sH%qlHfgP?_ltTTgRBWhAcG?6%$y7H# z8bnXN5mq$i7{zoe=4N=yZ}O=3B{vp8jg*)YvTjGhO}jhCdN3JgM_xq)v#gvGy$!CO ze~$qUDB{uD8U;A4AeT~)#ez&yZp9fKKU*GoqBj%h|M>ubCB6DK&CtmjR(HROmUh`# z(GtL*INR3Ikqem3PsAiLvgn8@=cXUGv?=m0tNNK?>iwf#)qjgdCOj>#2lw^c5H2ck zk@tfM99U3RwBeaa;t%_WcAUsabQzG*#Kx@PJO~t0nIFl+oP`>DH`n4u3KsPqGc&8B zc3AY}$u;Onxt{@bE=bUVN$)}=GLM={*WDN zrVaI}uW^di$mB2WdB#p-<#f-7jC<2o)Vs-MQIqwo`jI zn(nf}DLGA3G4iu> z4elQC4XifrV#lu328Gole~}Y=_Uzxetc0irVUyh*g>Kl3Ex8B22Rwz?SjV#*7NJIq zv_-dQ4q5%YG1z?!@ce5CgWIaj9fqRD-Ja?9eZhOSfut)`yIT zRMMrfqE48u$ktPjThf)68pE=6>E|`87`1L4t393T%><@2BkLe|h?GS?hRPRjZ+dc6 zMLeerjs$AM7Oxz3D}n^)W*k%HQj+b@4_HxFXB6<{pTJLSWHAyVgB_m(TJR>svZ4)1%A zI&k&CXICG z2VK|nbQq#>dK4aG*V{_&DLcEXNtBtA#Z+61(<|!}XxxvXI~*@jfsvU>ddh6Sn!old zO)HLdt*c|F7z6mmUPUL&7w{bc?5<>(4rE(>WSa-e0;4N=%)EAFYddX=d_QeM?eM6h zFp`MIuFq3Xw}c2`m5C8L=rKy{0&`2U9e@`)BuJ)c@{=@7tSL56E}n%pGrItSd62PQoUOi8 zYlGHOLZAMM%;;OCgh*-uL*~bHp-JclF6aB8c;^`C1m&dyy5pHyz+yd?lM&3Vn7*I* zwT35FGTK(RKnl7OGecwGgj`B#fY4OX^GiFj&Av84zFLvW4dwgfUl~QT1>`NJ$$auP^pd1jfQ_sSee}b{AB=Hj~ruM*e5v6vXM*PQsdanbIX` zp45_M`$R&KMY)8w2e$+3Y21BtQs%rqW7+~91$ao8N`5$pB+y$HqiM`}trh30yQsns z34DE-GmEOuRj-)6%3}Ooa~BI6x3*{Oc^Wh~_^oJC>pzP3zH>vAWE~Y{yDPUp47ozoI6%J|XWI8Z$CqbdWtFKx{_wZz9;d)^Wb+V0& zDnVs!Ly>d~Wl-j(ytg~X*+8e$ z(E?XrydAY!0S~7N=`W`BqjUEa<|8i(VFIrZnu+utPq5BNTB#FtYfeYAGwOz1^#zMK&X6#}Ntt8gX)VCiE)DyKj5%#p$Q=bDqZkT^=E);%a8&mcuKaJ{y`(*@A;3 zr#x{#y6cJ`DAsj`8fQO|Bi%F4YzKuJG_IXqq6I^!#mJc~H!&4Vu9O*I8Hd)bYpB1h zE2#wbo&Fr!Y-R?}i*(r%TX`cERn57#K^O(U$JkwH^r5^EAKd=7+GE8Ehg;J0d7C$4 zNl5f@!E7CUOdOsQ7Un3lG|SnQCMf@0y`nkQsdc$@H1YmwAGVE2E(WJQmc9y>mt5NDH;spObtiec)>EI29) zkQw-agj#;R)iPq92#l{F?ieJ%-al!`(zCK!qe{>Gy6e`=5X4X(Y+PfMBRTf@WGEK_0_0jORuy!r74gCV1#%LCTP@_@`o>!HP=##-FUZ47|Zu6?%GN_W`^pYh=sNn*1 zMJYB-U~V(klZN~!C+xAu+;;rq019v z9B4Is>{dU4EIp714rDVob%uA`F{J!rtm{FKL7%s-+^T-4hS|pIAQ&r`RkaUYQVD9pa6J%}X_82&hUV*DH!1(E zO-$xcdRacL==XEz^TGcrTL*%Md57C(Eg3v7Xsay6FYxqaMTJ_(5>MHD@Z+Rp6o(K* znh|qChS=p8MP*Uzyn?Qk__w_AK`HnL6u= z?<7O7CXr!h6~u&EYJSyt=IIH8%H^79Fef4sX7xdfy--S~3%as8x-zCAW?lj22IHay zGdoE9woc-!KZn$h{5hqH zEDtY|^3Yzu*w)1Jq^r=f1~6MKZSAs}rMj7iWU;!wKa;pa(X8N$=@u4T6k}90YwlX! zr*i0B(*L z3Ms7Hjns@LUgrn(klO+Un}=%P(qvgO9> z;U#Q{LVkU_Gri&4^)$OLkX+>O>8;=uQD{wX2Sbn^)CF&bEky9%ad8&^WHq&3xT%37 zfhd4~$Db0{ER+o6Pw7#mIt|`IlsfMaQw#sn{XN^gk);tb9CAlSMdm(cPk;SM;Jk-y4>5*WK^g%Tlp^7nOxO|SHnb$I5@?pwR-&O6Nyl{#9q1xk6pf&ERZ8*5yaGW5iD zkw+C@6-0!jMuX4+GfrZuyGZopvn?*EV%*?WO|6Qqj{4PSrU)9T4=r6) zTwMgdti_4P>Y|(Oznyf8Paz?kC@VBuf{Y;m{f!LU>e)>w8+J%|#p!^{JlFeXIX~6l z{hJH;^@fN*)@+Kxc?|xVNy<3*yptSbhQd6ptbnZ=c62MAx}E0NT>m9egpBhj+SI)Q zkp(7IGIZ%j%2HS}o5wt}ozU3v$%NNVV40(s|-oBOa_kZ^k(x{5R4ypN=61}MK$RGkrSdZli;oXT+d2mg-j}vDbpWkTGS)rrnaO@$zGmAKre|j#-A?)tqzn1;dWC zZ_kT@83Gv+NLt^Ox(dpEeHBH)>*03`b|a!``N%Z|BdTer2fwKUbjm{^+T?GHo9BH6 z$koY%mLyDY={FOM!pCSjQ={Z@i!5o(S6T?VrjzWf6ji2DWxJ*+jNVcj#YM$CabAU5 z0#yP_7{!X9@vBfua3C^qpM2EZZ}cltE=?Q#;!v*|Wn)Tz zSD`A5vUHboNr7E=i6oCsgxrWQM8<}P#=BlZZ*CCF+-)EIZ~`@V0O>yak=Bpom9k!=t|gfTRMaN zA?)8@^3!3iu^!UJPyV!b&{hkvj7*ld#u+0!j>3fb&pM{^NxsEvGi!Z2$BG+cx~0hx zkEr@|yCo=$st#8x4xK29`**CI`6(N|08bsgnKEdEONeumQ@eIRQ1gIsuzKxeO26GH zJDAa$&%e---}sO5Vg;Ro9rMs*8oRFp_=Vfpe<$yDm<(^^eBO#-1+RPN@ci3&?7}HF z7v;p$YmW^sW#pAj%P_5=F@A$fIz>}59sG$5!??MG)Cjc`wkqBA2?Uv6o=yVlG>EGO zcj9u@_Ey|lDYS_fLW5&k{TkYw=Er@5y6m8ekAno4VwX5%KHxR@+qG;8j3KIX#2&%h zrhSK|r9zxCGcWFgOVh;BII7sV^hfPOO)d=1^=1`o#aV+*n(WcE98b15)=jy%gHS_uGBg|Zp6)PmvcsnXPN{o9`bT>4Na8^P2 zYjV#^v}7t`S=F4A7$~;HP{7*&SnfZ<9GRK8SJi4 zR`@Jg=ez?vP!HE{r_Y1}Dc)Q9?B0pg?-V-JOBMFVH}S%Dgz*%#&GqCMO{bLT+K%t= zUpyD{ZMEyGphZl1o|8&xsIn3-bhtM@Y;*QRrJAy$rk16lKD~jDIsB}$a4mapp-qsk zkYdZz{X|kYYQMApB8B#-|2}CgP2_6H<)nC2E`lemoqInTNBpm^A*@53DAdVo^$Kdn zDMK7zC;q~|#~BaaiV;#G_b$YQLrGZ(^Cg_?mw52XlsPwoN=T%OE_0o1AUmV9L6jC*bv zogMB(`tv@-pz8{WwEr@=Kq_w_#n-RP59zn(-y;*TW7!c)DOD0tIsuNQ;QrIY6Ir}a zA7aQ4T+4>;-%VWk zf@gHH9PQ4)_L(R6$qva!B8vVBAk%Gxth#BIVffwlIn_sFIB$-!puid4uDDF=kaekm z$(JLTgJG?<5`I2zyC!hR(M9C#1_SnnC1UmpEUfzDdECZ=_Pw0ov&dxM0 z%ddcevEKC@q&=PZzEip~;G-#QA2B!TGkB%o!x_6DEiZxGH!Z1omu`u{ z=N@u~C1-B>slas~ba&Je3xK1xGB$t_uNCwu9cl8USh1$2=80Y2`f#~;u+)?Ya5HP3 zui4tpaaXVVczK4QTP?qv(|y2h@*sY|RsYn}b$I2_OzR&!(O766q z?G7etJW0O7*pfIP`3LjA9X}dd>-&>I4=bZQsvfNs*NZV{Xl z(U#2Y%uMzDxbQS)X@x5$>f181REp|>9cPW(TgH?@lD^VE($M>*=Sby9xmo!4r!mpe zH)0GGdrjZBpxU|an&RezJh_WIYS7;t?e<4+=@fG9&>gRK{llINLg9=4EWwPo-hTK$ ziH)tbSyL`JjBiCgw*r^gQ$Q`O{gQh$i@zHvih*FQRKrA!>I)Sps7B23t}H&AX838@ zilIL$QYc)dARJ%?u*B2d({5+#Od~Rtm?MQa2>xySyBDc*OSR`U zGWj;#7Up((q7&>FM)L~PN|+_x_{oz zF1f2vzSxu3k>xnEszEyoy1Cp#9JdhucmvABC%!WHudYJB5`dVOXk|}1fi35L%Te=x zBDaIZhU4mX#rCOv;5t2@!xIk2Ib+69#do?rx61L{Af-R3#1xwY8 zjq`DEufzn^v=raiER`Q8;oq*JxXwHr5NNoplxoW=O_{qH`|mpTY4G|@l=ViP`{Zl zvcsTtJFq>|%vwbALOk%-E?^f(wXtF;3Z_ESUKlG#mU7IT(`qA!IuqzfTX$c*4c=qw z?3h3*%0 zUWo)tC??c9_J(DOplW$vS&vmBPn)UY6%lsvq6KTePwd1V4!u_JLvXPxipd}5zP;u_ z7f(q^<>U2^u`ZX=a-q?)c;H*bO=-Ux*yZGliQeVoiwW7~!>@kb>yx4;0^%V(D&(8= z%!g7~l7_a2?i~qVJO?BkfGJi`RIGvw5r9L?Ew=i*prx`h7Hl<(OG*IcAja1wRnEH8 zyMa<)sWMKQ^dpvZD>7!h}xe82p(yQxqR08G* zf9NG}vlNB8(-q58|B5>BfMt99nXUOC!Ic9467RO-Zg96pC)F{^7uHYW5NGS}W>sZW zL@aX$S+8j}8SQLfxQOv;*8J?2E_Z3&ho+ceVPsOyR69MFx3Bjc;{9fsd^wM-VQnkQ z;OS26LN262!qcaBRfQiA09%!`(z&Re!!qZC#Or^{pD7?AfJy6DIwU@3+}B`du5aqJ%9DiO6%eb zoy>yHMCa9m$AprY7YC1obyD0E_leeB$eu~!9Axn`eP~uHUn=HiKxpi(_ z`s{fxag|(JPzw7H?+u@TrqAhvPr+?*HeISNbTO@d|6BUq29!Q`YlL^z3<80OFSxag zl&Y6_oI>e6!RO{A)_+*np{@7NpFg4ZKn@7j9$O%id*=dX*zc-e@31=h#EWfzjw2)vX*)4+o`KFbpc=vo%?Y)Pz4PU0w5k}bZa6|>ql&P%<%$gf`l9xRK zR{0)B?yxc((rC7u8d#k=Cxx;kz^9t3X-VkAC-Ez-^yk<6eV%*8uf(Bh9e)KQn- zB~-QccEUxry(D64in=6Z*deur%YHUGOEVKwcz`N}M(|Qu<Bhq~rox6A!|#0X&W$!Gd}pT@2jBm4wjI6c4XfOu(g3BcVX<5#nvY)bQ zuxik$)n(CH=%~51o3!Pz86^pGrHj;>FgZP?z1UxwgAxyjoVu zG%rxXzvbA7jQ5Ydd=>W)_-Qa;ECY_A+$oG3rY*4eK#>U?G5? z+*5nLdNb+{N~-ow!^dsI)AK3iC$}7itB$Vj=8Tr%_TuUz&+6@LgU3NtzsFJ2ZdzvM zVw^p|IX~GQkNvJ%gn&=^DQ)v9pGhq57tnhD$9A@FTt@;55ECT_&AZoQW}I7Rp8wpZ}s}}V7|LTzvW3#4z}~c%DP_Bdo+6Gu@LJ-97H(K6O#(MMIHMdd;fQD z${SYlCoFrc>TTz{Qdo2W!|CZtP48b^6Rk{r5h^d%Nz1%wzO|dS;wEvjy4|K3CAm= z8V1waFB0lc`>_31W1XKmd4rURRLvHteT2~ z37(YLNK!K>g@#SOh!WKCS8`Xzp6RBPsqp8LSUpdMx0UV zNw*$YWTNcX`oO1)rfpN^G&?aU$@%4HFtCzuadPCv;*#1)eV^I`xmV9zmtIQ*QsBPo z=#qi!lUErfLVHPBZl3Ms8vWj2A;(o-f`so zyXOxq_ofJUs0epJ=1=$Jaxi$%zGTh%@P?HCzAkniyZwD&mH1MC^GOi#kj}V7Y0io& zkd=qB0;LMIin{I_A7>9`PaO^Gp9RXE8k!thC&^&ynSARIxavhQB)cEu{V8xcw0VnC z!6H;q;a%Yr@Odqs=00iqx_#do7Fu0*;7t)|-4xIf{$9KEf3&IneGS*E7g}C`u=LXx z7Zvyogy?ruP*u>v#`77W&*d!7fP5osy$)sotiPaMW)&pUHYbRjul`n*t0Xpt{y&Va-8BW>28RG#e29V@{59V~B#-5(m$QrG6aC1v3 zkdAfU8BU+W+y-F|x()V_PoMC|&Nkdb!@`Kk$))9Fi_`#Mik+Vynj`EzS7U*7wd_vL zyW&9&q*Ek)zFw>}{dae$pz0F>J@$XCEMO@=g-3>jJlBIkKv^Qgf)=g$HX}FJVzJ(C z$$5wc00W)(CTR`)_Gp=jP0(cfQq1hQQxpe!L_NO`F2ps^KBv#$klHE_55!Y-&O1PT zlk@dJ8(&|xH>f@8%l?@5IfVI^GcX{^h{9$f_a*u^cwO)W@ke%M8Bs714WJ@969f^9 z`b)8=eW?D5pxtSNgv80779uA_)~nT$5Qr9}9U0&H^*3rWc&cB4O@1J8Haa^JCAm7QSyq4!fY$ePL<)w}w zv&js_wPr%zIa9Zs0Mw;KzdhRFnKN(UlqY5D&_9`*@-g)b&0Q&xq28) zG(!r<9r2*_rs{DVMJ9M;@t5~^xUwZpl~NX8=b}T0 zA{eV3lF{``I@|hUQxpNPY@a6llaOYLpNk6*FZk=*P{4ix$v{Bu4?+>Zb+TP;&(6!s zn=pg59-Wed7jgUDV!eY%1lB({t7(aSCFF4upJ?SX&l=>rybU5^iU zPd+bzkWZ4MJqW%C!fIAda0Ee>PrWO!nKptp4}2?7q%jX|{9W&sQA|sbqkVdPb_O9o zrLgMO+rk+=ZjhG5lA9LO5RairIR|zg^sy(YOH5m?n7no1NFj{svtmKw+_yqLcQ8bH zsOWVxV9#nGf|eR4j#+k^wEkvStxjiHh$1BGi{#mq%!~*e@#r-9VZ>pe@`Zo%c_gZf zKE49JtrQwOjNm%H@HZ-5B#)<7oI7H2)Gel~;Uq?~@|hPQlB=%HA*%?;d<+Pwql=++ zWFDCNGB7X~==JPS1!}TmD@y&@o%&3e`Rv zcS(e$(KT`{g|V|;6&ir!dfu^s>wy^@|20W<)uxh8K@=}Q%`Pw}7waa{y;;beV@-oQ zpX^tK2+7cb<))Tp*LiZ2&Ds5@w&2nLETnB8zTmyvZx2Kp%QLI^?0A>mp<5JfxOzh- z_LDL!pY2ft$i?cYwe11_iAJ6!M1fE@p|{>FkG5$#X_%Lql}CdeRudgQf9u7km6%m7 znKjX2v5#Y6BhdcJ$(&`Y7PCYXL8<&UOXq~YYQx2ucD!?U%la3zE-tYw?^YiTkV)-O zN@m5(>n?x7xADu=!rm7w8ynY2^m9-Ea64Lp0{TWjINzof-(^!r%zo}lgF5)c@?zUEtOaEJ}sYXL^g!R^yf#r?W-StFX*I0}>g9~L{w-Cy|-OSpghq-zM5Ia!HJH0e-#jYyb|H6}~_jMJ5vymRUEGnU3V{NXx8!icfyVTLUJz@9S8`%!g zv7X>`srU2$v2+zcS!QipknWO@F6od4k?wA!rMtV4Qo6fDN~F8H8>CCRr9m3L^X~u8 ztg|ETAPdiV&V66W8hGH-n?ElOqsNxL$zev^w|YE+ke>UU5K*LzXcj5yE*}i1MRODy zOIWf}{Skqss~J2PLVqwL@of5$xj>#omM>S!y)Ae_+c427IL;}XNMn_UvP-Ye3lVPG zb7g>${1dJ_SP_8aV0UjXEi20u%qbup3s^(|KSI6lM%z+Z_Tw8Xwi!Lwla@Lqi(yaW z@-Z6-hzEPGdf$5aoBaA)km+bt&^J3f+js&D1iGyrao~OE2w)E|3xP^lQ2!3RL|j{c zuF{T6m*LPTrORH6qu)ET#(v$Ph)n!kUE0e($A@9Gu`5$G&nn7C7bqK|K$~VQ=Cfy# zVui2F9LPeUuZNOqFCpVX^<|(yZT?u}Lw_GBjBbz8U0TRh;|qQ{R5>iK&q8desuP>@B8anFCN~x%1SZDdTOH^&3#dI@$9DR}yPPk{C&s_FR+4#oE^da1 zdl*x1s;C;4)8@{YV*G&Wv5cl5@cxS_a_w8vVh8{Gx1<{=qMV=qYN`dZM8cB3Jaz15 zjFw&{V2I20TE+R@{aJc!8`l#0hRbq$?^j_z)-fDJ=@ z!!&|-(*I7^6b%g4LODbbK!FkjP=wqzjW5p+i!JUf;O-Rl(?lZWKsbH{7^;iS2$qXp zY{j!6(#HTieGl^8Y$$O2YJ496*2@nrRuceW0lXOazQ4miV*2|;r#JGVdB{*7`)Mm?QHMU=oiDSTb-zU{v7`F+3n`gBW%r=hI z^t@V0nZLS*er=1rQ7G;DVC~RK&(jWPU(sFlW@u_B_O6+aOR%1vxBi_+i<$GUge@`l z9v$nzX=`$27?GEn;7&~F2`m#E;c?8!Z6>$tMVks*dG9Y23drT3!9?;gfy-UK(dM)U zkhf7(>7|CM1~&Ch=u1g z$c>Bd3%X{uo)USvZ4tPYjX0eyrFVD@7^>4fW8q{OuB8dcLwbx}kC%2&$(|jqczO7e z=x2whGT_D~P^<1sE+{CXeMyvYtJ{%PC=Ut7Vnf^3?I}yPe!H z?&g4!V87O0e@YKHrFkr!J&+l^Top-wRmm9Acdh+}@_T92TgmRO;q5`3;q8$p9QvSb zB=KRb48l;xXcV0l0lUlf+MYaiW{^v7q6fdis8Rn-^KTw2Yoz8+3~drUBIpsyYIZS& zG=^_~`=#g#b_37f6F0FE2ri zBRLER@BmJe{6q+q)cSA9`gTcHhE1+`HTs~JIij`zR68z7+H5L_j|(Q+*}zqU5f?)zX=e)zD#zphR8v9JOL zP-CDo@nX;ad9R8y(BA)HIg`(oUf>_F?m}2B2yNygFabU-_#|6kBqJO?_OQF=ERX!xmXJd_YU_;=J$Qs+)ku{Nh6Yd-2afg-b017qH z64|6^U<|bBKVyEL`X{>NCq;vB0Ea1z|6a9ze}eau9b6pU`t}IU{nztw-v#~Tjm$Cg z25%of%}|5Iqzz4X)7$G+0zcyzq>g%>Re!Gx$wln&$!W3N$q4;^Tq2VCCf89zTAKaP z_!K;7{YHF;cHB+Gp-Mcqb^38@N~K=O>Ir`2i*kxcom?toWVfs36|VA8)g~{rc-o9$ zjRv82d`6zyDsxS+AmzA9-4(q2&j19*3swOEG`f7-cLpqC z&zDGTOTQUTo?a!KwjA_O{XV|7j?EoXf8{yQT?G@z%#b!-|GCiU(!Yg#%@>jd$uo-bE++jDe*so@w}+o~1l3IC~HG6RZLEtgY&ESo4+jrhAxZZ`aD{7YKv z0JBXDUL>}3W1niI8p}(fli1J}jY2_X9mtCqG-F4QC7f@aZ?UsVq$-WQ>S= z#%d{&{E6dKWupaB$-s9D`os9DMQ6I{6-c0C3$MKLlmkc%HI%Pi*!VwG$#g6q!O~GA>xmRE+pq4!E@*w;x|uOfWJLo z&Ii^u)QMFcD-!2(?RSn6cqgSHks<8ukHX{XnX1bwM2Z7{@If{(lzXDB9igkG#pd0Z zDYZgP&BU(#u>)d3{-Qce4XMlz+Q=|D$p8kN4O`@{6DZS57sZaYI7%z(?ojWLn9N>h zQ!`rO!9p@#yZq!g3UFALy$TlFgewW}uI?Wm4o**FjxqNxJt;PvyqGqtfFQ-GbS$T> zpnJ=_xwYmO;|JqBsC5gDjIuZBB^JpMTN>j| zHQzNt9)h73PkF=?^NNxhJyGG?TNpyVeaCrj}1s6 z=E?8><@8`#v)aSW%j!NccD0bBt)btNjd1X07)Vmyva0!IX##*%S6A)1J9h%}YP+F$|YqYO1)-+A6#r{Y|Gt!tN7MGptW$@-3-9MNG^FR6#+?7yA_7&WK_T3klp@4Tq8DqIUSzEHs-c9rH*@k?ZS~o1M72bzQc%@mnRBYajO5wQBLlko%EvZxosz z4baW*dT}#=+25Cd+Wf5y55I}?$j_d+YFF_ZkwQDoUt3ApxqbGdRdrcT{A9u3rw=fw zdL4ep_OiDHTK7o(*_@0W%i2P7kNmLme%HZb&`gf^ngUcEth9%PAwEwD-b(@?;@EfR zRr+~)sm^`7rc@!2-n{=a0#S5J)hzbjybkYU`^4-=^|Rivow?YJEP+M2_$rPCr;4#I z&9%H6wH`K+9k(Hhm!Id@$zvgRKRla)0Ipiw=CCI?=53k2^_^y_i2r5hTI&A3?w4VUGBD!E1*JsX;{s3oV_f}m7>B9qKsFTdJrscxYHnC~v2QADATpPOX ze@0>*Thmj4cVOdVML_heTaY!zVu=6w@w(Y~O&^1$`>iG4Idt7Q>T>lK>bzM*sBH-c z0OG1F^Yx_`X6-y4?(gvl2`NkT2n+E-k5Fx@7pQXQYXC|M#4^s#&Y2(Fqd}P5&(-(o z94az^c7fEabcY}l3HoJ%Hj9zpxwd)PptlS}u|jCw!|Quf!cc3(Q&{l!gMjD|wKPO} z&_Cbt>r)7Ke1C0gTreHWo}2U})iHbTSj5?of2fmbTq;1jlAC$3Gj=xd|aq zT5mTaAP+UD(@c-zLxUoaoM6w5CaqcWD>-Vm!Zz075ldY8HkE0@$Z~fK@8&pPE_}~U z7W+xegi(48t>0OeoFnms=8+!E}lXG&OQ+12wz&kndKYWc{ZVp3=Xt9Zfh+ z#dMbNR`AtSd2qX2LNOH zoH*xfrTE{aaDRHZ)p|8j?Vtxgp_*YcF|yu7V6!X?ou!ZwNsJW?JZ?3dgsoV8P9eJO z0b8`H9_K@0&*{6%y~3mpi?~JF8MulE~9=t$T`qcEUq*Hr2W$S}jt2fhx%y-{2wdRyB$ zXu`|YUHz^0WnG~tD6-Shwlz?+{%?Q!_tQ7!MFthHe^@B~dXZ}Zq1^^(9sQ3&gOm#> z@}I+ib|E-`GCRJliQ@+(1mr_&FV8o3UFd$Dps1*+>oNf&F!|r(6=Z2!J@rc|%JvF} z_)sB`%)@tKnIRyq@dT-+o{(5AkZ7X#@ft_)Zb%U<=0Y;kRiCzZGwgT&E!#sF0|rOjp$z-huI}!@ZhIAiHg~4J zw3u2!N6N1iVkAH%86i3-1{IC+VTG%3wDuT(*m0s=*=|WdYB8v^V0X(wfw&4Dv-Y*A za2__|FTKW}NtpA^Sb{1S8SJJJ9|nj|(j68SD|ou?nc(?33C|wIP1&95@=$h>5MVEhrdR<0U7Jt z062={drMD8SM>Tf`*Ai?hBKi<&sWSi}=i3kGK+j12EJJotI z%ux09C+XulJcOtMFEc+;KcHG|z4|Q+VM`#t9!6LZQ0sAlixv#-$8hFdm%q%i6^+n z{cd|mFQV*yf;#TU-90X&xGiSqD}!MOrYt!To0+PU)2Yg92^`o=IB43eYnzsBL4=V? zt&bf+-Q88>2u&eT{TtY|hjT)Z`UmX5DDX7lYPGf-Re>SYFq{V1++Jw#pGsXfTsVz# z-xwmMGHb2yA38iV28mtlstmG&jVlw~<3Obod+)U$KcRyQuSs3rt_wdm_T_oFFg6x} zj)O_>Zp|`3$)7_<2cyqJL>*wMCcJ#TvkjPjU zm7_kUivaL<9O1xbPO4;)a&gDzza-Me> ztbqUR%o9I(X4p>0s~N+isXDBfl8%XL;L9$GlLI!?MM);llDUt=Rl(IKJ~RukK&m?$lR%FQ8D^ zkvgZ+YBSbS4QPlwN^O2UyI1MeVUP(uX-72GHq)RqT4S6J1 z(Bk`yz9|Q@U@g=yKID?+vEmB>l$P!p9Lw@cbmKKeQcqmu#C*iATuiPs2OPF`LYyzbaP4*cu%5#d;>deH!JW{0MS_cMx(Qz zp8(g~X)`lWkxArL(ZxO6k%v<(9dm(4o{+1_OqYd`5!fk=O`B}EM)vn(b1b+pcUa$1LmtrA`i zffmU_coOlArb}ImU!*0@-~7mL`6^VyynzStee#rO4UxRtdbj5`ow}4Jv|pEO;KCE! zU8pzV_n<*K6^MKjaTX=c-7PCYU25Sov3aAbv%WZ-ALE(q3Ip^*0xhdM9c^?YFMIzO zU-qMk(Cx1gC_JpV)`7*0!4Qr{r}LUET$KT4pth;FFZrU#4l!>fzxLnQOv=*94uJRU za}F>1tE*kLj_FZv@pfP9>mYcVQ~y|s6I+s72L3^SD1+qV;QC_FwrU(Wvv5)hB3a|w zzl@!kv0#X-%kmh+KyKp>{+-LLX{a?`;~yv89<69nc;6awhnQUWHem z1N%wvoq>HrBF3%2u||SIiE#fH3HVZ*FqxNixU}CUIpT3Dw=#hRL0y+v7GL9O#^HTV zZBpiOUu%4@k}HZi^PgMy6|+AjgsfZZx93{`F_OthRBEB7tT4vyG^+Y$s&#r~bbX1Y zLOBrwO@~+(w{~P*`}*gBaTlqL;Dq_*bjvGz9iK#l8KF#`OE}8y>)j6h$SmAyxk_-V zL7CG`IMBds3zQ7RAKX|!ek7hyq@xC8 zq@kG^+`MaAPR@WM5g2X3&b&sp#wj2${-@0wh%WnYeEBExbeR(-_9{G;JKo;5PaIWr zi~}>tOSJzT%L}#tI#MI_EhtDd`OmZyFQUN@Hky zH{k*8b>7rEMB>EqyvcHVkNV3!f8Xj(jqKq~8vuY-Nkil}Op?^1VD%}KlT^d;c4OZyV1j7G}aee%)U zL08jW0~VQS=jWD{tY}XPV?70~4(72^fT{5!m&yVMbi@|Jupy|7KTBHFHK|!dHD+t7 zs;(7B#3E9<%&xHYd#lh;l@CNf794NE;Yc4dlUolpl)?~ZP|8#_@Guz(+NKZsRQ`45 zRtU2w-YjqEd~1N$|M~Hun=?uKkAT#IPE^y?lDb97Y&dV^OPiWK<%f9 zNd7Kixc3_|8M@(sjR4y5qTkI8!746nAIx0EDl{9eewqK7Z%~oZj>0W7(;@E2OUw@b z#2sf8J^b62qf*1((atc$jk|L$JBLqi=gzWti40}d9UyMtF$a`}5jx9p;$9Ju+ysF~ z(CzO+*UStKL=Xh(1gj)zig&Ppz+4tT^zWax3^1fXrx^>N0SPUR~Zj%V!tJaf#80g0Ec!QASU8-`8MIR z%f8~V6$OYI(U?2S6M2xj0oi18p0A30j&#;G&j(2Nz|5AbUB<@4GYA~TNeH29hCJ9+ z_Cl?|Uhrj=v(?dngk-e&k#!FO#sRIA_fdN3zY@fo!aRmsV2jg;fqZ?=*VB`?`~@sk z;6U*M3u@#?&AR2|M*n8*cbYQN4a-1jsK!RF^Uzh*RnOEXZp(GxvJ_j%l#!|VD{5Fq z28&X*GluF?e#p4UcYMY$*Vwt7*~6UEq#bh#mBk?A7K2|`C1-1=DNqjF?-ur*zGE@sXv zdk!0W+{A4PGhDW$rIiZOMQZXU*4!pGzoMYE^QyOG`U!*ey;a0EBhe4a`niVnrW;nv zvEgUR$Jip1=dV|XU1hODL_lQ)6w$z9r={?SAqZVZ3h6F92;QAUXw!1mE2#2nTYG#8 z20D9)MwGnvm`hM+HC_W*QHmH7ljt{QaITv}HC*_BXkn;IU@77djy zBuA-R1-@B7EyJ!YtNq#UFj{oB98}Uz)zkZuG6_#RvgCqU z$R6I*`PlU0Jpxl3MJ&Vv)$4=N%4lOKN^a!V45qz5UK4m{ky(7jkg^DI$i+NgGQ!UY zh{#`Z$gYsRX9J=sE>&t|9fWc2_3MJzzT{+X`B5EF8#{?a?J#MEf@%~8E2d2vXQ&G1 zE~C@}9@bHCG$GDq{fe5_*2o#-wK)PYCt|2xt#=o=>BdVaYTZ}6u(>OSaB+vKbbnJ> z$99~~Lpc%61L2rSAz$rL?_d$EVOX5Q@Va5X?!6i<21G6@~1EB zZ5x)j_sZ3LN^s@Q{EY22D~M`{Z-lv_%~n57b|o!Qv-lli-*x*&L0$NHmObsy9pzz2;beRg!Tz!@K3Fv~&a*}9G+mG5-cyD1o;y)h z|AVes%*TGn)CGx)t!Zo=1R9tBt^vU3OGXocLiCH3nh6yH zoxU28YBV6*pQ~2EVr+yS7?~2}?+3HiT|~z%ax}?@&|@AGUA%*agB!47)ijy^HZ8O< zu2M*Rc41y;QE<6i`at}AA332~10{R>t&F-$4eI%DXntsq|AeLLxLHV}Gg)&5tKJ7XIC&8Z& zM^D&5A}v|u*H|`reQbOr>KjGJm^Y4XCq#k48$OiPM&D#}V{(pz?|y>lH-Vg@PmC(e zDF;VK5qlSah6FM?IXOA-qZvLsbNI_U6v-?4oj{q%)I>kZP;yz`mFTNXLExTeM)d3v ztT>K$%IvC#FPnY|xZqRM(~W$5GQmp=5IsXyJ$Hp`qeS)MV&lq1#?I=4eCy$_eguGA z!#w>0ZM{5zx?2(~(V|$^y#=Uwd`3FVJQm6>BN{3%ia7<6M&~CkqnFsR&k8oR))|AH z&#{nKx6~N=qRGJ>hS2+10XWqD%Y?~vZ?i!67CnH+xia0_n<)9skxP5nxd8}MAw{8v zfF~N8a%7B5FI4Vt=(Jd~!0+wFc{S0KY6-~(JZH-3$#CS$&5(M zF3YwMp&>{b0iwn#Cpi|3rN-ya2AK1%mBblcGGEfFpsESD%f;Gb%|R$6Xv}l$LFH!U z=l3BP*ctzDy6v3vKm?K1!K0IGLu6VuHh-&K0K9|VahK6C^xsg^eDDt9{{vR6YE78Fi4mH1JKMbb*JnglM|}6W~3r$}`$HY^t1$yggZDo|q_!no-dKdN6Y<%mGabjSsXT zKjYUQCR&G_?Z>KY%>sAaf~l#*6=UZ!L_}rZ299Prq3ke9L~vzO{(QjXm84U-!~A>F z&}h6;ICX%f#igi^a5rAeTyyVeghT>uZ96fUO2L*!%2QfA;53mPxFp8Sq7v=0N~l#%T z4alf3JkqC6+6PDT$bc3Hbb>UNN7(A{L_>^v9v=AQpIc$z2Y{6F%}Hh`VgGkQJo3C) zyCT^(t8dfwhu&-Ewx);#uqhk9*hI3tRJUWk1b)^Ap=%4r=Y-yDK6WjVym>^p`0wMX z{(&^#$4P#{Hl0T3`xcaPn&a8^`^pU#9;%OmTG}NOjRKeaEVz}?eqE#D*V;bbeYS9L zabj=7(zlYzakWpax~>(9M1fM#G!tkWdkbV!0LN+U=H@2a=y7gT8fNvIiIK4qynug$ zq?eHqP&r;%UoVljT0l%YE{_H0jJwPL`X?VtC(dUv=5aw1BwIZNe}kpnl#|ZISOOHl z*gQ3r8sXlpdlNv_B~KK12{FX}{q+fSDLGjT{@9c;iD#AR_$ln@&xAw}15)P&j^akV zw%G_7PMSzS!KBYcLXL@pL**XY>Y()>KIepqW88gY)(vCgf~^)X5q4 z4s^nD*V6W7mjmzD(%r5=v1gv`848>WvH$g@1Ko3VWhI6^GkD=a8i{UO{BKU3fbIF> zsN?wv0;2+)pDD@N9^g)ZWy<#br_~NR)y^Tf6HT^9pjF*4ByvoGPOI>y}q=8kYQlQ#r1q4KKErjwY)b{=1kr}a(kXi-70lP`UMH7q(?4{pm)|q{ZaF0hJ}@L*4_DJ^{SX_X zf{mvLLxIMB6~w`-IN>tBx_347q4dy)xbA zY|)N4KQw)%MG@EONEAOrb?1}a`wo^3g&<)0{zJR|p>+BlXWi*fCxU91-*YbX zQAf1%^#ET4B~;*qbGMzqQ#+T>dAH2A<<$*;mtF4{N|-eJX9JA{_*7KyhtI4macrHJ z>1rdU!`9T{B@CsEET9WqdV#!rb=TO+iE&>+i-_38(`RN!sG^Ksu@OL%nzFhMZ0seJrw#PqqeQTd+A7Jp2OpYxDSN4Z$j&GpmDR2jv$ zkbv>TUrSDsF=Ka00nz!NHNT4cYeQ*c-hH>+Et*)pztiy_7|I;@ixrF|h+uh39wn4U zdwN*!=@ij31(op}DZ`G9h`34-{vK(${>QLleZjBI-XVml=jUg6-+bHUcL_t2v$L+# zNXll60L;k9K9v+!kpbK0e1(Czl~p(pBZDJY+2!)buH*dN5&VJhsK164d24kw7AzI$ zB8)Ps!nZ70iVC?sSAmxT7!DdSUoQ}w0nAvp(Sh7)NUCTW=#~H5er8k^&%r1PX|a{B zliO_t|Mv`<AX*j;0ygO_Z`{>y559Z(ZVAo$|z zXk%c0nG>8WVy?qk)lXPHc8Tvl4pNTDBX-{H$YlMg4Z-`l5;LY`$vPcPhC+^@Mk0m% z?I$$uY#miRhHw*I8uM3OUBpyc*?MYYV*RZ^@91rmSCm9s=xId^(?~r=vJgT+>?hU`0>56Ij)Ftk&r$v}-P785fa7p_ou8Q``9`T264}0^-x!Z5Yid`$$BJJEeNE1h0m!51* zm5xt}SDW3IR3mjOxR)elafxp9Aug|33t9_W8Ze+Lsba8YwoLy z5C0amrl%=$He`$DexuFLlOM2Vq3`OQn>JLA7BLV?{Lf{HpKH@WGUiuM)cdgp7|ygE z^uVD=Au+)UAy38-m5-9322laHP2~*nfAqNUTU9A$!FzC+(-k1U2kX zlv<6zR|0_90Q@rX#TOXQ&$3Tda1W_?O%?fCv%U@`f^y3K#m z%)de2eu?5>-x^8ST_)BUR!ed+0T;#8R^o!*HNjYTSb6u!3?Zq|#EA*=jM}JImrR>+ z&6DK0}wJo#HGwQ#TN z)ouKa`@G%AQgGCTPaXuvJPM$Gvz0u)*JkMBj1@r$>;$W%Qbj{EF5Sy-iRrDDwFeUR zni_676n|Zl6*nJ)yBKIS->-dU>0vVIB@DQ->TgnK(gHvFI;_zWty1vv6IEBV3<(t~ z^dm~~VMt{NX>$kE%ry`U!Ci`~Ql)64-_qsH?X5|1FMHJ3a{nI7|1&`)uCUK!V)nD& zeXl49#n|fUAxMQHjx1wL!cu+|MWEowiY8%bkoQkDa(^9=HHoe2XywFQ&Js29-#W7pp$<)km+ zSPH2!NE18MfguhIeZij@(w=~)IZ9%y5VMk$vrZr4-~t2F8K@k+1RBOOAcQ;vLWn;~ zcb32nFT3maws;_9cw|frI|oN^+s&Vdydbg3iK%-=N467Yu*S9m zy!StZ*4p6Vt$o`yuvMi2&D+J0qF+1&)#X3yd=0LFc)4FlMj6T=#LdVjbGk@@!bGS$ za3}QQ^>scp_pZlswF4uv> zx@t5aKr{gLeY$7Dc7N-oWibl)~QpR%qzDi2yy1CE_ z{w*!BYW$GgyklPJ!7e(+RvZhwCy@2JE>LqTe05A<0gPm=qY`MZpCCU&m9f|J@*(elQ%zdfpU@mx^+ z3aWYTYhjLl@U`dVy>FxxXAanBASbyB7^ZWaC zgC(vLiyqNa5*n%8gvUCra;}akRHYcljj*QKlUXoU?GROCP!NSs0ur91lT)Ph9N2nc2z@YS!~Wv%7b)Fdn}E}}k!7~(hOg7iec<9R>S zk?J2lx=~y|?y7 zv0!hXO4ziXFF!xF_smRTHQ-RjgJ@V$^uu?mV{F zhZvKNSKT4Ijt~~IeHAz{1glOgV|D5KQ7QfR9*x@fX*F?`OLYez_yh8V>H{hgu09DV zjBY6facy&=t`oF~7<<>jW(|%-HGXkR|FMTKZU$To)8${h$-BIVoUiHilAi-^n~5w} zGS^OY2-#k7Din6~#k6VVV2VINR0yDB0if$ z>5(vB#Lrv-!#Fz^S6r3)l;4__<8sf-8Z`xW?MAobw_l>|A`@m%C4wg%vhv*`Y)$mYyy5t_I42-oNp>)q~r*K{k}G7d?!WsTMBpI#P1R*7bX6L`^sNW>xc z_DkaSyD@vV_wUbhABNFIB)069xgczMvMGdP096y*pE$rR2ds_xDs=7t>MAxK1i;;P z_3xkEzF;BxoE2yL)3H5efeE0tZ5FFxCoWU7vicl}Nc?_gGl*qI|Y@Xxuf|C=YM~--W3&s1;3W^OyunK4r!Vc6*w$B(LqtYaZpc| zw>x@>B*czDPqJ{&8Ins+u!OJ;XB%tXwE?xmaF=$>?0&tpGQC$>)6=;IWc>E9>&wYQO z(xz-`iPXD!Xbr-65tYh@>8^SqGFtCSl@zrr{$B+B&{@0lud^cEJ|IAY28CjmXl)Bs zWSpOj2Qp6NztFrzY)ICt7wF+^MWlo-ZC=(dPU}}Ku4W`y@ewyZdFI$H)|R{qz04euN41YJk}AOo!Rg@`IlmW zN*H)Uz8QFL`Oinygbd+~-x!LMZG|FfXbJ_!K2o;bTlu2wGTuI0_4rgFBr_)krkgx# z3}la=@hjdf)O$YV2JdJOHuUqiJFjjr;Jtl3DCYBbf3I;@Jmx4Uos=U$yDGu`_~mC5 z&*Ngj+_0h}Tj$IiUA_PUEt^TF&{4C7m7cj@gAVE1+==^9{TxBVU7MEm$3La)Gxr}9 za?!BZg@k)AhP9UP##6)P0@9>4jbOx%>g?Ux2nSPpjJ}}d>f_Zd*gMO8cqd{r*HV7J z(s=A94XsCqKct0XqVAQJp*(#_mt?JGh5D$pW5YE6cC@T?$^z_CvvBqvBqP>uG=^t? zUL<5V9Gsk(0g4%-cKU8zmDi!ydNB>*p+I{USi_r|nku9*zw>45dYQ3n-x)zB1`(PFyOJD`*i`tMWH?b;3@`yv+Tf;2vKfaY$qs6 z6v&3P!P8NPh(OJ|dIEls*XxQF9GXu{PrcQp`@}z{1^Zri5)D8u5zyxX{4-opDrllf ze)|w|2WX@DFGtz`lNEmS#wCXVrpLXi0g|maNs^8At{`yAsGBQpc`UfyP(8B;wzsMF zCUR&}M&9%DM`F-_?~jB9wHd%yU_&q59Ur;7?nu1Pky#_PfE1~xjp$*lF{|)0ryj#? z=3~b23%9Xg?9RKH5$$@J=&(lmL#1xQsD=nXqIZie$?~RCVFB%e>~T!e>*Bg5{I$V`RjsteE}9!}dK^Nee{=^^F*;ilvRHXArt& zrXoTv|IC(6ULEEG;UP4%KJB$D4nbLQMF~Ti1R;sdb^>qVo58*kL4u<%h;r&lId6HQ zMRQ||T3RxE^q-h;kf!Q@ti642!&^Q*3NAz>dwjf{*5MYOx_A|D^HuXFM!UwFySuxe zPD!4j*TafO8VO2k&iKFnV%8tWN)>jxX%!dqPc{XI&lnVUyI}q@j8`iZVqVMb&vHvz zBKvT4V-*^Ir#)DbKqAg!l3RX%vSZYkdyx>w8EU6@XNfI*#V*u<5hh3@8j9(IBT#q@ zGjy!dvZXzN_d<+1p4wFCx?HBsP^Z&CKoCI2nfIRkGdsJxOT6K034e_BwPfvp{ZM(c zj6aozIdfUYSB58}zu#&(YZp&A*JPXgdD*l|b2d)UP0P%ivyix9fRW0&(n4^sMV_xZ z-{PzdVXG^cwj?4hCazU;(FS5bHG1*4$K~bBy7O(qXG{Q%0?UF5%{o_lJ^F7_0miioFfYStBQfdNB*9jtutH$_vpXlWym9Zw@o8Bq^kJw4fPT?Xc2C7 z+Cc($PMmDG^!n=5lrVjg5g24ZLY(9x8F0k&cwOxmuGbKAx(8z;h^cA z&}IqBfVy{Tz8Kv<2#|b=<#rLR+kRz`{t8z%Plkr38)`!0us>DWBVqVu^r|Qswg#0j z<{CI1WK|rf3E+XHp<3CHmbkj4=LZe-nX5W2ODv4aY&G{53v_-u*L;B;RZU8yeD;$P zl$vw*e<^5j3|g%$wJMqZ{SUI%>~>^jsKo4_~eqIG7ZE)vI|FRniZYX5!RgBC$T8ke!nf^(rL*)(oqm0 zbKg(%iAGZLLZ+Bm|NL`ZF44|bdK?d|*Ib9L_SWbE72bgP&pASb??thu*8bdx1O4c( z!`UwJxqr}_FVbjj)$)qv?jV6D#Kgo`S65=s9U_|IZwUyxZhZgv-sk}V_852#JB#iV zfu|ZgX=lDUK+EcQu`LbBAc-daAi!qm!frKR0j}B)?kC$*lB5M-JBMC##1aA2!2*ad zf=44FD@cgRc*;*yrc=b;H_;^ogMG4T+>l8l;MyS3VhbJqf*|~a29l})2`Ea-NJJ99 zj0U-eRi8}2jR7#6%Q1VugpP-nj;$>BKj5o_C`1GRMR&%~alrs_6G9HdX{{?tsw>!P zi|KtKy9SUDLzas30v6veP*$A64`cpssx7?^wCgkjzgAjk+inw^(%QObG4yoeLX#BG0{9% z1x$=oZ@?g)z&J|19C6VqbDH`>_D^TeCCn3FOZB(v z2*RQd*<7zvY3cC3fd4$Vw9iFc-NnmZxthq#;rDO>0xBrkL~FAgTp$nra`AD#>r)VK zMk3`W+n>&6M8ZppKT!)z@8GO14>)AMy{%Fc6S*-Y7u(Jut$&{~@DRLFWEOAwqg2XZX)h80&=SM#SW8IRqGxSzb6o7En@z<$+o+MXf!yd@Z2icAWtRRzir zZdRwKAvZ6$LqU6cQkhNz8$Z7s2)%K6+0EizVnVTm*9)!Q ziyd-T+TW)tSPT00X3yeAFQJ}(*h#TNR0Kh#{<|P0{#bEmJ!(>p(e~jcJa^Gg%}2SZ zcNKrl6F`~(sk&hp`jwF6p-%It4#T(b*ZH^2bH`;fUb>Gr=+)x^+%+ppOJi&RVUMx> zXns_uQ@mQA&)j2iF_L!02fC0krxE%l41FR-eN%B%LV3AeYdtYwq>85pJmL#0I0zis zdURwsgJw0du8*0zh#kX`vo)Y*WHQQXU>oZvwG}+9=k^FS`So-D(LKmenw(6ne8LY! zSTE1gxxH2t=%u{*o7WOcogy74PcuY?eTAb2A+zaBrcqq+q^Re{M$&5OYdp`^g&~|5 zM1Dl*gG1`IPyV0u`gSV@65VqGpqlzOKS;d)zV~~Y_s>%Fzn|uRJnMlV>&oxLNnz{$ zVN&{V5ymvs7+}DFEBT+a>Z}XUKMdPH@7iDJbX@B|o_wI@ z)DiG*)889TV(@u7GT3{0@_#XaWG*`a@g*2{ZvUDKj@UoV*`GH}_(lUKj-10|MbEOALUAt^JO>%La>hLGeatVHl-LWYA zT+ee-t*+_W4P#;kgusCU5?1msPnF~i#@ zz=vRiP6LoQ3Mp4~wkF)_Pso@6OpPse8q`+JQdI|WcYo`6XY3`ZrF4QaZMLTE{+J@8 z$G<()j z30s=Qre$^;fGWYsGFs+Jv&pt$t{A-)D$sp*bel*RUoyg~>?F1AJ+%8lnkpXQwFFay zoW!{nMsE@9?($6`lb^9c{8~HZ&`TXl*Xc^!`uq(w9hKUv?}({U&fhmMlWceYSzvEm zVGD65WRWJ@jjQ|j5B`Qi`4znAg@+VK7A{~1FDHm(1?wdE3*^tYuQsF0iWKP|93K8? zXh`u9P-N4v_N=*Ss;}=qJha+=X{fF3eSG8x+fArK2~ZSC6{vKfCoYzgASAIq_D32B zYE;8eG15}al(y{B2{xzwlHvb@ln>=2C`2LMh$cPXb;+L}?9xr0KTfLvU!Ns~=bKIec%UW}p(uj5wS z@8!l{Fr7zELE!>e+O{}99=1EZI534VP-$sSX4Mb!Aej)TxE@e5pi0xiBxlTQp`@#% zZ4bzjTW4>tICTOv-+tmAhJ0np__FusNUopGwWqJXEW-a-I`4R{_xFuwQFF<|72j}V%UEarhWRA@eKyzo9g}?jWlCu0=#a7J$T13^TW-&(wzBwu zD%r|L@Sn4WR;7XQSTX13_*9MAp2vHoMyKM@Ugm!O`;(gUnNxQjfkb#-u&~@Pp<>FQwv3u^T2lJFQ8y>dIvF~oSMKdL2 zE=jL4MH#1m_OfW<(7lJ*vgQ{vsgc?Ay7=ZjNqTCc5{_Dpn!x3RUU?>7^D}JbzOX-E zIx(D<=DSS|`@eljh}USX&PG|(Yq5NIfB8z$qLqm>L<^)^P@<8m)sG)qhSprCk0GP5 z-MI6g^oj^x2v`+im4GAd;TI!yOs;`aysd)U4jrzVfEo{0cI8d#10aT^gs zc6PkZ5*z?@M0nTlJncpz@6T@tegjZ&2S9z`b{FZ!KBv)TRT#%*iAE1=DOg?s{QWsY z^kRPQrzOA)8$(JKel_y{i*x=6o7HnifUY9~sbd93rsNA&r%2nzmWQ|Zhk1)8OH5qZF zm~DQwn)2(6gHY-BqgRPT^$l%uS&|GSgM||t5b*b$H_`XL-m2449z!XDidTD>Hhi3v z^}F3$8#7Zr-iUizF7q6pcaeHEHHox_IeC7s)vm%(gCT!5Sx8Hjm`QJ4oMntK4MFvW z{$&-I9I66C^-RyHgY-wuZ0NI=ZqfX$)gZf^Z77L#`%yr?N{#brZ^gM)*21NECR?*P z*?Dbx^EL)E2(tP`H5*G6#iiRM!s{3x03%NHWleMoYW2Jlv4YXn;$RCya zql4S$)YB2`+t(4nJEhv{PQq^=E|pu<9;EQfBJ>>e3;d4p$#o72BG@+96<{HUFoGDar}U-NiAww zlWLpNb~36~Qoz3?ORt8=)_``Q>AF-@!y^LY$0r;G55-*WW$|Lo;z_D14ks$t`^`gU z#^+1!rX=#_rLp51F7@Om=C`K1IEV`4X!fz!^lwfjtq(}QBazcfi(KRt{fAyeM+UsK zp?y{ofhBF`prSKO==UMzMmBonr|WFx0Td?cs@exz>S<)1f^(_Y5}2faXP~>1v1l#) zS?PldRldYucj{6dF$o&ouTsxBG_}Jm?YSgW)0K}2P891GFxf>Y*65QXr;Lw2@(rZi z=U45s6R*63iH0(Ecz#(=Z9dytec8bn-&gLm@fz;P-;rk;`JF)+cQAR0B>&Ou`YBIT zjL~=Cf!`kC-W)Wax3r{xjik!3QBrZ8tAypso1b!8|0G6n*q8kiEQhoW1y!wLD*uUJ zE{BH+P1au8MV>_%U!-A-+$JZ`7L5D)%p65Mtv+=kQgBFfqXSFz%x0q^!x=wkwqb_$ z{*xK)cbX}+(8K#&_V+{EnHtIU;j)@yF26)If3J24p8CH8qt3i2Pa$_jBhsDiYRReI zq0yWEp@mG-S{ek$bFOUyA*9bZzAp?a{1rO8o32%?R5aR?9sQG&k!YHZCWzKMo!g=| z@9gJ1-3^1vsVvx~kbIurdf@b}c8c|g{k1m&rA9V(h+P0I*$6-Z2E%#KS7Ahi*V)tW zA!mOOLVs%N$A8klaKY6;-CPQCXgG+koWjC1jRp7U7k(F&l-Qr2IKl5=r|?<3@Gg?@ z1=tzQOiewMksKp;26W>zH_vHyl|w|)-e{*aNSf& zz(0mw@`JC(iT3s{q8~$6<=i%l8vgkeSN)l|={5IB!$Y5d$zm|R+_vcB^u0<`ylk^( zpYFvsn%e&ynS8aZrRGxh_-d)ZRj?_=%Xnn;&w7J4RW7e7qv5tVEt;7oR?=kL-H5Bq z1&SGik%agrj|&Eco=K}4 zDH#miwXX}2-DrF>vw<)mj%Z=oKZ2q*Eji=>Prh|DZlHJ(n z>XNtp3FF+ozuzCN_ac~|eR2`wpRU6HtySf&%KSramHEA;SH*TiIjZj-OO@S_qe)*r zA6(CDj!*N9If#r}SKF(R1IriU=B=x{O<>767#dVUU44~sWNAh-etBbQW>mzyw5Lfu zgfngYK=G|brR7B@qmXoHsYUjYQ1RPIwt^=Pp}LQ(@342(3@9u=j_(sYrfg@+f_pj8 z=G-D8-USS73=}2p@I;snP8~FOlO`0yi>qo{NO+a8YQ{(|~jL0wc zeJ;L(>-vImqC^Yqavcw97@iDTEUWojirN~sfvbjPP4$+ow19tyMz>3+vej`x?v;%p z?_}0Z$GS{n>`H&_R36PgOzUTwl|m#of`jXOH&s`zee@eXQef+=VQD=bEbF7WBl9Ov z8;@Vf-}o+{P@&>0C%VUXw&F1zpoaW6Eb)AehN3H6%?qVFtMf4ZHB4__4be#FAfU5~ zEp*Hv?d)GMe|93fx@ruWZDTjLMDSKpR5KlCiQSEr{urZJk0BAeJ1<%>9?>MQw(mkl z>A7znt(yCE;PG+4%B}Y|zB=tUx{2#6O01jUIIwO!lg|k71uT7#Opk%1F8fpVe9kTj z0y)`MZFTkFq`fb^C2|a8rqHzWA09LN;Z~w#St}S8S1=yON;QGxc9whjH|=Oy_~H$c z&Fc@9!WQP^3m(0=0QZ)CE^wAYr{Ta|vFH0YD(oJmc(0xCyT!2SXr!WIaglx?!HGlE z5Q{Pi%gon9!Cnia-W6_Yl1GsoEb`Ai=kJeRWs*MBjdhl-Pryx|#69k^kZtiO?rGc? zQd&_9(-fo)c~PJ&Qcs<0tdrKm5)5*YN^r&laz-zO0%MEJwZq($##hbwcVgz%`-o z=okYs!SVjwrdM~ejG7VtudYMRulAOfpN6<;qg4(~hF}>%HSK z|Hj^)&wfR~O&H6eEL15<_8AvF zN$0JiU2Nz|Wo5guPk4>HSsA%h0?WIzj(Y4;`QsFI#Xwrp0@`;cR-19*tiFXEz9SCY z?zLOXWIlUjnO6VRe4K76iLi=phUn+dWX#O&;ADvD-K52=@gftQ#MKug!2hmvXwaEs zPU$xFkhnnJ<8^V!}Nzhy8?n!JTvQO-=Ro#I}oodF_Ss#LdRA zsqIs5%uP6EpnmFRLMShW97->owad@4x#iQ}DYlPy^9R4&2X%D$AnOv1=?d8*{^z`DK9! z3P!l#V7sevF9jn;J#8O{XtxV`Zq5nfJCmptj`MkVh4Fs>xM;da7C<{C^WpT+=0UV0 zpPSK#;B0Pt>H?|ZmA-gzOhDvB8I&Ic_+r zK|z**tt}k!pfLvm2cEJBFZrM=`~$$@0g0QR&j@LYNpNjs;i1;{!LxVIk>@>_HsKut zwGfDp4{9_Mz{L7uLB$l+oT;=f!RQ8RO5H|DBZydlaNGks)-EKg5O6XiwZN@`n7H5K z+^7JOc8&F{l)SwBPiKQX>ic2gcm{e(=6&K+G6_bNHqAOF_V$RUDnc$7sU|}9J(+95 zUMF1hN7EBW!akQOB|0rKUt*s=-L!Qnea@2MCQ`TEWvV$CshqRB(vbCAT9IcIy~kY1 zEXW>jrP553O+BuumC)9kFY6AWnbg=5B|ka44=xxtaf(EFy>6Jj9E`jLS;XTN-&k>p zvA1K`w~~nj@Fg?~KFH6iy2(x?5lV0m@fcpqmx>8uTEF<`|Doi2Jvys0y(G1?S;gS; ze0=XqZnm>$ytJ6lZksQBzH>K*NWT&DYYqkO>y4-}cM0`)CfP}*(t^QhI97Tm6e=$I zIY?dCH%6V9jAEu+6qMF$NzH#Z<dYZkio271vN*$aBV&|-p3X1=mL#+v4 z9{)P_Vbkly`El4ji1J(KA6Gcs)Aw6<|9-|dE20=rOHeglnt{4!oJ!7CjPfl!vggdv z548!o!Ba`4J5fTOK3OtWwaPJvKMB-Y7?qfa$A6n=8GYczc(QzW%Vt$e(IQkS{}ybt zqZv1^3LNRi1>^!LW1tsuV3kznQvi*%#%DjxU{1~;C~xRVbZ=~i2v(kbRif72a-%}0(UC`Aq5)knWMRSh?RS-STVQNSwI)};T< z?l=XfjvY$dx?0`iMM=C8Tt*gFHtz!tvVl)a)3G6#ADKv4v$h#KHM$wxmOjmf{&kLU z91G|s>UAXVaW5aZg0*lgtf}j`nR}Mlxsmzp8<@@oI1!R)C71ul$*u(*x9g7z_-je^eM<)7I7ov?a`& znI;g}pn78Ki`Z*l33wi{K`4Ew{t9Tx$fM{ARp&^bHj-A?zl~VIbUgROblg@*p!`^Z zjtF@qk<|#Y_(qng{QT}SUuzG@XR7NIKJxBcf~OI40d#vC%3ps!mYxf)eNPGNe;>B} zh=YULhKt@r`%9#EGBvdnE(`CiC#j4$DQe1SulC0x3Ogbua(J=%1;MVLH*d^V$Rt*+ zIAfhIV2Pi65A&z?m!W6LjqXK{!U)-kA zZ~}KlZw*op)ysN}_E@ox5$+mphZ% z5-YK|t%{_~dfXaYM$@O(qIjX|z(w{u%SUp-fp7Q@jVhG}fjB^}L3(>2X@HN9KR7c( zsWJzKVR(7`Y9{CC(ufmtadF8q`VBuL%$bPJu?gLo5+V{-1-v^y*p}dOo7xDE5MUBu zKL*~`0B;1x<`a*S7?{?p;SHgbIZhgyL^uER^XrG7p*$)jqxAbKu^h%F1=F4?9I8qx zmJLdz;Wl;>{7cwMEpY64XS2)lna8EN&~EtB1QZpGnUa#_l`s#gY5TedSM7E`02*0# z5&5t6g^Wh$-QaeQo@_<6h7`l>xu3Dos-{P1`ndU7eFKX^LQa>dPijyWxm|T^>@TZJ zm~J>ZIUyT5kO=NYq{-zbrlc5x6!!lqt9tn4BcJ|XTU)SzZ(xDTM*L#YA`Jn)aNm`Tg6lA)jh5sLlr zyX+v22401TK|gyttD6*8q^+)#iXyjZWTg{K`~Wb7>0tFjbnSQlE5d`wy@qPgRH!xm zL(RY<%b~E&OP*v|I9{JX78WwWXMb2PIa*H&TYLR5aKPgL-yV#SmmTw$DAhXmMJ`E? z<|3o%SAH&m0rryY8hrVg(?cR9yjDZ_%5n&y)a3%1B~PHi=_Tb8hX}xuvZC_L0ULii%H`bG{?V)kl?880E%O zyBAMfxqT~1ER*o`&W=z?*(1GHP(_1~+~BdBS;bPckVymMHvEZ|J2ExCCbZIe@9ysY zYjYDyZN&Y`{c0ww!-Q2;=ZGK17buAGqe;H}7;B(?Z;-gXEmx;dv9yTxuVL%`{2!wK zTnex1nO;kNg9licxT4YLXKZvkQvaN%o^mWjjZUi=r2jK#Uof9C?((}&lfl>;E3|3H zoB8^qnU?0skeQ4Rw(mMh=bO0R337`8HTx-bEESH5>7r3v+|->aJPlK;&Tl`gXSb?e z`+77MlN`D8b4(384J^y##0Fc9_j@2clgOROb-@p(Zht^vs@8@E#vhZNxn`*A0E`+<+p2ZhR=i!d|v2eA{bQa>CU8uGK6fgCR( zrqEmRFWYh?&nyE(ns^hO>?&fh-tBRd5dO&<$DN&e;{X9DYQYceTRS!7z7@G=y?4l+ zAtzx@spnodZ|str-Jc=4C7hy?e!T4s?_kS+&gwtL-Z+#^tZoY>3wVqQs*1jAJhWU} zf4O6v&PikDWZCywIe))%iaqJm?_AB3hPulzs~?RXw+Nd&?EDppYpmzLAmn<)GzR5( zwgxRjaYjQxs0Zl>JRkViuOkHvY)_uNrHXoZ|%! zG#~K7kYG8K%wBc_4-XzBBzZFVCVUS9hhU5lYG`P%6;A)2ay&D=M8Vgtgzv-ND%%LR zB%}rc@-wX7dm(3wG#9jh3Tv-m2Hwlji14}6#nSgn9M0&8$vCnfQ_Bkb89!nmDbDnX z4jfoH$`RqCq*%l%ccg4BpQ%bk*kN`Vdk%*UzhD^?ol7C8t6Lhu*OQM&z0uUVL*d+0 zA6HP5Ln2j^ePyz^!SALvcE7m4rpRugio~`g^LwKxkbWqN%7}I4# zlV~798=0C?7fsC>3hrUdyvZXn9r`lENDP`LTU%-JA2`O1d!J@N4O_peWl6DymJTi> zZ^jSboLsIwKGo(Owq0D$@VhZLZMP%}+FAt~tfD9qlI}kIk4)k0l>evtC63meJ92~3 zubvU{z|$esXwAZg*;|h=XWh9SK{A zmqSVLQijmEcx>YIt264v`C=cNgKg7#-l4Cqs69MsECJ?GYtI_ZNQ+1N>5r5h%ZUaH zj4bKvIKwyyjHwQECV2AaM#BQ+NatdSHn+>_YQL{S%iGH!>N|iFrlEG8oG}`TyhTUM zv;NcF-qY!NHn;2p?W zAEgvno`Ovu0cIijcSmREn%SMqbYOU$?zDb~2#oTG2Y^SB=@?*-Suu4beSq^i+M2(x z^4>Z04)-}Iy&!2l>uc=?H2KDQgjH3f)7FF2|7>nYA*~CPpuvKYmaFh^7c4~kfvlc> zx<`S!3tK91 zHR+yYBJ!%gGn_{H*T|BYwyuI=cet)AaV6bKg)9HmXpUrar{*|Y8&68M=P0Q_u4Y;R z-NOeow)@vT_#dw_(azTmFI>8O9=JR_=f6DFdOMLDD}LbIB`RS5IJWx&*0}FZsYO#+ z9doGc%~Y^>&wtNhHk*BsQL*>t`{~Y@HL5UifNpveWuPQWL}o#BgOb_EDSyM!7kdGh zHgElIdYz9~qw(2o)L!rle;99-PCIy?tC`~tu;4WDEpzHzHO1Vx$7^n-71IAGy3G?- zGZK~?|6C0oc_qBDj<;U>Q9)1K|1M)D^4gx)l8@~8m983z2eD7(vlM>{G<5&8&!X!2 zq;oXM@`ZJd_Q=HLn)m7Z1>PIiE*4C?$!4P& ze?NJ1>zh|vyr2Q^`n!x9hugwePa@K&xu`DrI=ZiiNu6)u-<>)G)LDfIZ_u$9Iv zc?rMAj_S|x8TR9pj3aK%(q?ic&hN%Dl|lU%xbt->_By;%iP4(@hu1S+X)jTsj< zhfn5}LYT_lWH9VGN_L9-7U)vv{pKo=ivCs}Lr6e(D_Lsf=9=bSYwtX882RGNVZx5B z`1M^a)bO#meF1^V|C++7{>Np6qgs&$Sfe+3K-kIw$XAnLgV6O5ya%RJj6 zhvE8HOa>z1>pIKx--a0)0GS0i0h|UHjg%hX^TLvflZQuK_T{vTqa%<= zb(p)%!Mka0&QyU804;VlHbNCE(fLalj1hqp_~%Hc6XI1yRus)UUg3zH2Wfl-;efgC zgXcQjH|~+eYO#>vMU0y?s9TJ=cXG=uGLWS9xBO~rg9~+c?q9^Bk2n|Lw4ieo%Pl=W zpWm-;J!Ohsp4byqv36{surI#FQLIGH7dJep&4}IfbkVN%)~e0Y$QYi6$sS+DH@C7JoqjLLsWaa7 zepu8g8x!ZmQmA=$#Q`$nmZK|!z6J7^yYJf<(cVpc_}T21R@sLZ;>3OZG3`Shx;GsW z(vQ$8#}xb&GtTObnBTt3Rtj<}*OSgbofxCDRI_@Csut}##`28}UKtC0JY#34NFJ9Q zXYZBpN~l8GDZ84d_=j@zRCP|)59OwU-;z=UJ+ZOAj~=0A2f1t7O%0Y*TZhbT$^J8& zEuH;$Bt#Qq$Q-fL^-R1(GT-2e$N@o@UAYyGaDXDl@80H50huDEaAhO=K%iD6^3bv( z&>H^0U*AI2$n6PP;l*m!a_0Zgi_*}%vo+BvdYDP;5HI|x(C2o;p6q3cx9Mmv{u2J; z>rv~p@%`$|@q<;DA7X9zdh>_83Fib3?AB=F5udp=;60A`T)-A57Tgck6}Xev`O&J! zi7`aiuj8|`!~~0N)_&@X7!>^`L$`EFZ{|I@+-1(WMgGh6>~CBwsGU0Jm?AedHl2GjTRU()eB>2R8pLU$(i?F z5cAM6XYYN)5y2g`-%x$MHg53-1ZNAPfG8P$S)sJ~ztj{QGs~y5n+d3zM4L%#kB!>Y zl$4)fEk=r35sR_Do!w1&90R_v=koG5z$FX*p?u|gkrHJ<`HAW=%-Zq+mfFCsAOKr)>LEz;p;}a)a{jgN+FworYIg&;%@f zw~1R$=eh{v!N_?=_(BX5kYDw1Z9VQc5vpV#T&L7sqiMPDsdL@NC+&5q+PoH*>>rq9 z*l>@L`?;2Mgwx|Q!U1NiCP7Nu;;heeV$vnD`KQVQDt=UoOzD$puUXjd^Tk%}{Wfn4 z#uDadi6~qBq9*xgDeLNW7+Q|3uB?9)To(8JtyVBR3<+IbuoIHGF!`S3Z{clkx#XQh z9g=#=J(m7(Px__2@VX<#4AH2UO7;4QhC~PP-O+C<<_hLjS$@v>98U>mcVv7c*eya5 zRIpKf@Aq=q@H5Y;x@cnu^d1cNk3XK%#dmwtxz>E_>(GZrri#u|jH#tD=A(*Uatov6 z*7K60pcLKf;Nx_#l5PGjQWJJWnNJ-)Z>e3_&(^AWXp`-{?8eEl#(jR=C9X&mMD+dV z0>@#%T|zwLam)uM3+IeaxZi4g3R;eia$%rf7Q5DmQuwvm=|qgHpV6dXLs!W%H=#M7 z7kY7;D=OUdZirgDdYh!&K5kFw)1g7R~ za&moFSJ#n-&EkGmZ`M7h)!HG0s#AvIR!znjrb7c4DK=FCzWUp?o&#&M$4*eU1q zo61otqDd`G(Q5m#gSz1rDyZrhVz1fw6m3Njp128`j4%1hPwllW42=hppZZ&xNknv$ z7Dw+9kKSxe!o$gs%6G7G4sKwLjTp_34XrV0ujYDEFlG+R0@GI1oK0p1dOoZ|WDgt3 zB~!hBgLrdxK7T3rAFWsS@<10!>p?nQQ3{q&M@L=41MrcS1XBf2&mP|TZ$X@`2$PKM z-jW!iK_O~mPxJ#M{|Y-x_rIaf2!UzWj{Hv$6y)_C2Img|>fB(8`p-XFTcmDq6GWq@q0rCDcy}B;$JaC%-+Ml1_qJ{D8AI<$ zt`rKXj_?%yRgqy^pJ&gN)Rt0}c~$JM38t4>NWlHug}6EiyY+Qciu=}{Ei?2P_w#Wo z{9(S@rgWT~(|=_tq4R0?NjWBcWTuZH<>|kj+{#=seE~Yl75@=jOrh7x`d!09pGDuB z{`u%UPsF}Uj*}~j3$7BGqvEHiP-;%UIEu5?eK%&_4#oDRZw76Z6M~22?IQE@47p>fM!{fc&^EK-MjCTp-XND&1~v|E|rX} zW1BHosegjEeq$e*^Mj+W*1YEOJ93t$t#$1ypM;V>u&SE{jCux_qK>TZ>el1BE_Rw~aV9*cPP9lGUAOU9P|3h^+<(67DVaTI zs9*geUolfHzg)58)#39Nr_Ec^B(ZM>N2AhGQnE@Mb~{f80O$NbF6F=bFl*B;I@km)!Zw^Ld2&2yROd!(8DmcvRSWcCAFazFR`aHhqF&G0)C; zuGX${SF}e;JIZ0vw6#M|E^$qs<)}$$3&;oy3HM)*v{T`b#lBezyB$4K<(yQzOg1lCRgrC+oC(;&)H*Nh(3m)j+(eP z_W%8G_ps<(+p71y_K225cY%R(GE8d9xN|)`$$M}k>2UY$r?vKo`Vu=qbX-Soi#Pfp z@k{7)4%0s2LU$p+)slAluFUW}qcX%&4l5;S@x-B^CNqvU+erDd;U;Xu$m$E{*=Tr%uc_q;uBj|G%i~tvz+zrLno8&+0t0?OBl;8Ba z9X*E1b80-cDtT7)c?AB*0h%;44<4xx9R`ltT+!^_dVbe+K-KHMT~-W+1*beV{5?^% z0?AJ4UMe4R#syen^_q8UiP_7ip&;#c|D5L~_Ft$>ImK4?upeD{lX<!=|gn$UjQRhxW`H}?902HDv9SM^S@46a5j1D z@yF+qs$zx5$BpMsCpCx+BM3_4Z1$(PCybF>n2&=(7oh`xiS1-v?2g5 z-N3zxSmL3fXc-O(BE}~$Mm3qfs9t&SSKni5wLi5s8H5|eB zwwzi6>e??m#3-S~=pQ^8u!l&4w1c&TlY;;{5B_sR0=ekAJfb<9xIEGQul8+ob2Dcr zj~c$613cJ}S!_M@Xzha7jC6)VI@l#ZIJs!|Il93-AY)sly8G>YUrkds6JsZT+-SPw zZ*(F)>V&<~JXDSUTPwU0QVgBMKYXoJgS!*q#qLqW*9wSY=tVyNFc=Mxv2DHuV6S?) zk&=$Yl?dW%Xr#6-;bNKNdb+wFrWzIk2HZ~c9|{FDDcD;xE}V|sc@Uowx%t!Y9nE6f zm$$DpDxp}xUj&Pi8v`#d&yu`w*re(V8cX4}bFtcBjj2o!Ug;P_xWA>yZ&Fz(O_zM3 zP&&c;*KU8Jf*DMiiCWirQ=b_u(v8p!^94kI?dp`uk~F@WwU3oz_~A;3Bg0$b$|s2q z)qlp)Zkac;?h9Om3E`5rYs02e^uI|IP zG5xL^SEv<#nIzKEi78U1jo!%`oRZ})b`2%;H_i;(sjVeErez+dE0sBEdb+ZQD!k+4 z)Y5ZV|3hJ$Ir&#zX#eS6B*lH<;J=&^EWeGb*tJKY8L%sn(}YlC-#DQ$0~I%OZk>Jf z&pA~qWo8?R=ohXmo1RD>`EEW?qfe$e^2oLp@cZjBz`6 z_VqaUWJMCP?Qp(7c1%3S&kW{`?$-Y8OF7|mKaG{xRB!vc7AHP$+?LXYUN&tavvf&p zjO1K2QO3r2={UVKL65z5g`tM*xpuxZ=|*piu+l3u`IeG?qR-svw5}P$2`r=Ozw(){ zEYtUh%C0CC9V!m3rFIhuYKT{X1@Fis=2UGOUb2?6O!!}%7k zSFioSDZR9G4~l$ZAQA*&ae`-iIzeynnm|p~%VSU+!fpua0+EGj-I1n!3%c8Z%5= zcXd3G9{1x}HsucP#u3uf29+dAl`b8HYcW%H)i*1~l_mcG^dG?i7pd1`r!~*g#!}_mkQ~I=!gmMt)SpsQ2Ghf!l3~gOLZI z=q`B~qiYq80gyQ$Y=O7scvTx$Tf5&hZ!6(O;Z%tfx9G^et#-%r=Zt$^M&!1{c$S)G z243^wt`177mBbWuGmqD2@U5E0KUyo*YM2bnPxsfj(pXt-J;Z+=KnN?4|0u96EkZ@f z-$=hn{?_Y27JBfTu2lRw(`C-GXH9z~@s>>C`q``wv$d%Bs(oz7{Y8c{CwIousk?r| zpG&y)v-Ot^`60_CkrNa5qbVeIg`VT9$;)>rA?;(lwhHB&@lypqV2W!=U7d+a zUoE#hiw`ZlmX^+6pFJW5Aw+#;?~^je#)Pn=N>pn=C|aVZym4lOU`;T z{7IM#ui*?ia%M&kh>Ae;@c>($|3SXwx+ZiR7mSvv2mJ}r1Y1ran{$J-wx z+4U3A$ju$zC_d2R=|6wnR+aCCPJrR!c%9$i>m`OpUo=BM@_}|j3RZsfjVwH| zS6H}J#uAT{(9<3fsPKB^Uk`EWaa2c}KJW~@iYXJf_*JUAeg&Lgv?g()11y0l!A6)* zDziASQnX*su$bE_SRFCn>eEWn`1$o)llihks4Q!_Mv4CVUj@ahWtN-DVU5@4sPXEw zSl(I9fbSm8t!><22{x?_4OcZpc#~z}12hHrMC>b?r4#DVQT8jP{67+256BocS90L2 zM+ia_x<093t%aEVTQJXlYaIOAls_v5&GfM4MT|CZe2IJgHa5IcSbT0G+raT=fOq;B zoWj?sp0t5#D8Ud788LAqVp#P-iKo7~`PIG(B>#*Ao5Ax327$Zi(c*b?mi_1;hRUV# zcL4Dp&)*j?822Le^z~_nSCfR~DV1_#qTu2I(v*me4J#7=M9g?KRt_t@BrPXk&7Hp( zi(i*_^#qWnLThH!(z~b?{vtQ1%pEYfRu*AM^`(VnHc2S% zs|Bu|tIX16)5?~`QRGle=QA@;ncES>znGj##-ql~s|gmAq$CyNGYB-#)KD&BKAP}$ya>T={UsTf z&53!`;M*;Xr8f;%bYMSE-N2Gb1CA^J(e|G1U?1ykPc-rFYps3?B|$|@Z8xywD^qmY z?RkZ3WQ%@%OmCswKmSCCZNdgFt(vy_y+`QIj+2{Pua|Leb;snTH0}&wF>^Ej zfHH=f?jDL81Oi#yY+{FY7&5FS0hZ0ngYvj+D?Fu@HxvAiv2%h6)?UpUF3+RZ*V>dv zR%6rfI|NKTuXN!&Z}ck?`f<|6zR~YKVt8BpRbI)zy$USKvlcDsV`;u|_mgymYbKn& zn2KT!Gv=)U#Yq_rP`k+ql~{j48;COgPHIdBOBiF zVgL4`6v6J0`6oN)kCLh?85AZ2{L9_|TMZ;LA%R~pwHfrA2D@z4kt70;$+0}(8-~7> zGl4*(LV$@9O#s~>ydMhoVEQZsdw+mtGNEL zTayRXq}(ECFNBqoatxHT-$?1&6fh-};4<_{9m&Uyxk1`6hlx$QIw8LxYmq=WYy7Xe zRv|}aQ)mAlipo9<=39?qEDG=oX{6}TyPVcHcGfO%?kvDcmKOgxH2%P)PY(>$VPV$+ zX;J%3Az_Cf9Iep$_n-N1V`nG)r2g0$_Jsd4$|2n*P|08Tf;>G|&oT0?K> zKH(g|+5PI0`xJ_4XJ-1~JKVl~`~6&qS<;ewrOJEb2I>Da@qi#SGBZY^jPoZp&UP-(<2|=_TBNM(qE4}a8|6gaU4Xf5 z_EP=IYC&%Ny!zIjK>mR*V(nwaff2zKfFUd47QceawwK z)5neZmr|oo8&(V1!5ZB)vtNK|J2o;NX-wQYboc%7KDN%=s!Xm<6k%4)zdC+Y0$L9C zCV7jYZx$i?{2}{FayE7<-d3*0$O<<>X*qL95X+E7ShYbLmpe-{m8r17%iK(k^MPG> zFbD`Q%y4p2Q&Yj%l$O!jD!jy7dv7Y|PjtR0N$J_>(2yN~AwaSLTH~nqrlPzk)fQQb z1cMzho`Ei66;UFU_AHa>LgzI(rOjkiblK01md}~kP8{@3 z(AGP9<#P-o$zy`mVuM{dDemPzi!-Y!5w!MAMl+Pv_WL@ta#I#>z`a^(&ab7a*JLWS z;+K^A->k>(f9HWlVk2&~Bbh&?(V^NR0n<#tMnnyI%^H@b09DXFi>82$U?+2&5o>VrFuyN0TAaehsNIJ@ZfEtgovwxf6 zA$_s*JRWKxq~o5cf(6$dKro7|6;)LQeZdVnVs?R#D;xg(3`0!6lMTHop14G+CvvC8 zrM!>+G^)H+Ky$*GsVGPF&(_!-l&|vFSlwslCJiVnv${58C&>^+7bVHu$AC*;^XC35 zMgB;cC|1V^)QZTpKueaFK9{~k?;8@0mM3?fTnCRRlAMN2Q@ChG8~9KGi3cj>@YGZf z!r+FUim-9~9<^O@7_Yz$-qO+n?56B$593xyUcu5Ejy?Et`(J|@d4H6&)QU?=ID~}c zieE32c`uT|Jc)Gl!7B+!O~iNt6Hd^Mp$}ng_$Ai2m2x_I4!z)d`=90oPZOebAxm;a z`Rl0L{VVpbBCpKUtZ!{)!)pz0c<{Q&G=V-w#Oc0l4|sC$Cz$3m3SD2?x$A9He5hVH zhI~V7YZjtBAf+`l^hVx)`T0lbx3&O--@T}8jbbF5Ju)%T)g|mhNzffWw;#2dWM`GY!zF&^THKj+5XCj>GNSW4F6}4qY@%4&v8dg1Vl~g8Q zm%(4VZzF;g+qV6vb_RG7iv#E^Z})3ZN^u(mBz%$?$LIlbJJ|z4h;Ahoi_V7EV;u_x zvc14Y)46>Yx95SfN}>2eTs0SU4&U!r82-}zxI*5}3qsf#`H)aHES`)jCAD!dD0EgD zSXy?JN+-sSCZatH3D`bHze2~Heix-i-{>RNcI!P?PR{LbTM?E@$34E%?R}_<$i5(( z^iu883B12{U|EDhCnaXm9y0z2MYJyQ<}muvEGE@oS%X7jUsaA?2-bd;$cT~HDom-w zs9Dj6H$n!UEbq;l2k;{Qb1=(LIKr{q$~!qp364kFe#!&jQ5Vut@&%5VV)UFGNun z9OUK}kMMKofnU{9HEei`NXlX;RuON~Ly>9gm5Y>BEoT2jn`9n&Iz1g>f$W@c3qWJx-nUf;$)yg(xn7f}wUkfZw!l%yHP!@mHY`#FkIl<5P7w~OD`(}7*v z9sv6Y#|+Lt-YT$Zmsv~z7%Cg8yMSh#doF@%PEyQ-m+KHO&-{dEvJ3;U-vAuy>>iag z(?1g==ysp74lCv?fExtoV;^X}mlyMw9&_&JD%hz^;a0Ec?TMY4NfJq&$*tsQdZ+Q% zEQ2aZQzb*PGBJyLbl$vp`zHUYabT7nN@>L@gdkS^UfQkAEqh$21+aMrZ{Tq9H1b=z z4;NVG;ceO$ITeP|NJ)K4zn>wslSAE;h$T-ScZg~6NlLi9SWlmlkTBEzE?;ZWy~+9H z8wn+%BdXJ^eHDZp>5sCxGVkcA_s(-iTx~j0q{YS{O6I@a=`@QQ!BD+EcZJQ-dKC=K zd+iV(mCYmYxz^5RPtW0nN?Ci|Had)!y(eVTRzc#BdSZvJR7o{&EMnSNLq1g_o1w5! znRd<$uU#741PY}K*dVv;yxigS_ObJ2GT6K3H*Sfe|Ne?1WA?6^y80JqEttZcC(|SF z^G|!n+H0t_x;kX*HdakD6rUwLC{iuR5{tHSnT=S5Q$6pbj zf4I%j?|RLwQv!JQn5QqIF=VEGHVEV72&0?dkU?!y5XhkLr+k^)1Mu7X*7l#UUuMj7<8|DP849BytZiz7utT^v0*Hjlnv~CbGR)M|eDl5dB)AoPbEvTtIZOUvR6AXz z`Z)2K%TSaiTK=JGMiMI{&mh{XsAs9kj#YZ_Nw0*zYWrR%4H6m`MwMiB=#AeSTy;?R zB6am{!M=v5z4uVp4SI4_!hl{nAJx(VKI;rFRqwolqEzjxt`4tNERvZD9TJ119*Od6 z{gTiqI@a4r$?m0lgqN-G`l{e3{O@>gq9>j_tgfn=mr)d@{uMLWR(Z29^g)%4BM(&Q zs-#b&jVSm384aWPHi0u;W#d|SNu18k-HNI?a)mXDg!x=t--`ELJ$@LgcTOpPmeliV zK5~<3WosKfJjYEy&x&Fs3Z{b%RX5v4hO1M+5nRD;TaAi?ZWrll$r=Ac3nGib>Z8||I>MdP^& zAz$S$2m6*DflAphS#DRaf}Rj!S(aj{f{Z|F&iGEWx|^fo36x)~x4G-YNEj-GS_B91 zmu1SFJkHeiTl$?)UgM5L7W*Xx4jlLe2b75pT;uV)6{dmztPDlT|IM14^m>_pRj|hx>WXXkd66v zehEf0WCj3zs}WPfc+v8FYlaU#U{JoAn4FwhT6%*Pb9s3gR)%+UF?JpKQNZMdelZic zPa((!=!?7>DC$^pbbzua!~{5(^o+=MotyhRp73#TW?acvE(HYmpvjkyl-IM+u>aaD6^E3A^i5nA-`I5jQJ#Lg}f z#x^9x4B9cWdqm8(u&RV4$$xm0T%VKNqGA^IsOrfPBhHkW$?sxKpA)Rj6$`5pYN}hP z1KGltL4rAgn7z zcDrx4J&M;_s1^OS_KMiz-l^Aodw8alq0kpy_EGCBGUC~bA^G45OTFdjL7`zhN=&Ji zQ44OSF^-Qxk(_88_;~GC=J#WS%?ZU9IOMS9B&i+Aeg(hwoOkXzcN`2~8{mDZC7fv( zK*z2(9j5>7S=?0XtS-jg_hO6Ur@w5hg}j>HzLdonOZ@Psp1<;zefGm=c^?=c@Co%P zB5W#tePTUh8iZNBkMP^a+`-V)JYIQ^i%{^q{OlqC?~ z^cM=xKA%W;o`$Ts9ibRn&~i`eo`{Ga3RkwW^L26CbT&QZF(}MMR@3Lq`SLQy1P@5lRIs2@=_HVWKjxctPE-tF)VTB6rflA}8rwa<0?Keuu)BPpT?SRjpe5PP&Vm1wK zvBiZRXlBqY_%R2JRNQSL{ilbAE!}N+udo?=iVR7h)%3<&=6ij*^gg!I+XhA(prAoW zTlth@3r^SFd>A!mQGB;rt|^e&nVoVC@HdLAzRl(3YaAF&Zl&`}OL!?UUS3{!Knj4I z0)-5yg?0yH$SL9^J090gFE(CQ{EU2UQIq_2eqpa5(Jfm1C~A^z{vbiasrpdsG(vXz z8M=+Vv}-Q}CUYq?kCPI3$7Yi1W4c0q`Zw-*S0t$X;CVcapR{61Z~i4&cW0bw2bx4$yqP;?5y}a)MR;~WJ{xeh)D8* zZd0Rjjy6e(EgVKX2-;VuT(jSbBU)+a(B{@9U!~dU8$Ma-b+n^0mO(Qf-pga0JZ!|% z{=QjPe5ERkjYTf)>l^E3wpe*eh^1@5W+?kVSO-}YDs*P2Y6DAH8az^?$i_NJz7&}) z9@L#_IuB}!@sb0@pdm9b+jAW()~4FQF=b}jSeo*S^^f0#kuymU!TY+4B__hB5_ zVR6yPVTXr|Ci~UQCY;4qX7CCd3dH5Kk|XUqYx5kd0c|#4NisllqwwId z#5l*-50=oZ&JS#?#g5GKGmiJcMxU=xD}|GAJCZ2&sin;{2Ge*?PQ4uJB2{GI>+I00I8dwaWk#sC;? z8F~8vu$_!OW>A0Y+`f^DR~jM!f#;1L@%AxS`i*?X1zhC8LYjn>O5?fRe@hp>?YQOF z1?^%Gi!^A=@}>~vevJb5YoLMyGCT{eh{i}ZRa}~w)@r<*p}j`GMPa_#ng0SPl>~6RBN5{}HsubG?bKDy z*$jSpO*Iiz89LvyB0C*}^Zr*du#_*9|8<6anOcf$PYrUngsyf#VLazZOT|SP4a*jb!$v^1j;<@U0#=|>JA@Myy6iUKFH{3BT zc!+PIWQ_79p%S(1JM@+v!eApxkr9t$`2g#j|EZ*7SXkSdhUrrbw2}oC3MF`SLQ{@^ zqWW4{$`nkPVUDcO!{ZK`15244uB9BhVAm`#|!xwmn2qBgR{Ie({;v_W@H3G7+cb+rY==D*; z9~(G!qBvv2kJ{6se2$E!E?@tqTxePR`D!@)iubM{XxV2qqU#FeB<%bhijzR;B^(HW zHt<%+?T(p>1q9qxX?@kG&9OV37r}{1pf~a2`B@oumfy@H8bjm#iyH_bMd5;tM{s-jDUG;9`>0@9( za6AN&MHH`in)lO*{{&$IpEukQ$VVl<3SuTQTI|!2IxnE@8f*c{x6{d*>|>b@~_{9$p0oqJb6+ z76EAD7;0dHHKT|ie$ zoJ^M$M|CuqI=bAf0wPL;E34he81hI`YH(I5S44tHIB2f4(*4vZI?esirK$Wp z1{7;u0kXS0wIpp3dk^+akYx0TM1~}5){dAE-}KQ~T2A4XQg(vcC^&pKL95462Snl= zGg_@;2bzeft>&TnT0W~OJtp9jCfg!7*SKg_fLdHpLvfzXRG8ZHdN@s=wAp&W=)1gOGEN7+t=1MR}LQZ1NzInqovd zCLFY{Mh9w+-~~ZkYyOh*K$*Bipup7bj{METBo;y;qF;c1fwPy|#!F&vp(^`?T^h@; z)4&kbiYgNkbx_=l-@bD59GD;0g7w^cpZr46o{lD6V3lmPbSDFw+y`e&Uyo`IMS~m{ z5;yyf6KrAlOis3!jd&$rju#eGRI{w|qtDqBR30=HpEF7%^72iCgSfdlrxroAm)GD$ z+@n6~RJUT9F;>PpLIOic6!n=OC8LSJ_nvHMCJNg7zdM0+mo#pTOkJ#g92uUsGqIkKM%-IjG;Bp4d9M?7p@l`zej(MitgSaPoj5+8W z&(wUrFiw)kN#lvs*uW*iGFOrmt8~OPzQ84`HLgM=k`(^@$={kz%=2><^7Vn_}+3zh%28?oY0pb8?$Wovq zB7=!N0y1cDGk*)&00$?dB?z4bG031rB)Ol378jC}!%Bi1rw!-`AhHDx+_K3sCu+gW zj_dLAo1CpAFZB-06fD-7p#j51_gt|HD2G5W;2y9=dEd5f?7RUKuMXbUE&wx(0*C8c zi~wlfnyH}9n%9lFF<}Ckg7tJ_Ar*tX4mi_h!{3J?Rk>$IHQssH)mK~#*anX-*4m^2 z^d{(Xz2%c6NWqMt=Mi<^poWQvOrBBZn-)Qa)xCP)GQYSikj#94-!DaGH}=rRio>Ls z=)RbDNra3XRpqnC;Ci&$*EPwsh3`tx*&*c%{Vk#C*wUb=kVhFj3}9a(1ks`FpI5U|4!j@p^Vjsd{Q7^yma}$gb5pt zG*Fz2r3mHi?wof~i8el#=bxm(8mC?kCgz)`_0Y{p9)&CJYj;CA2;MSNnqY? z&$TDN(tFV91}`(sfny$sKm{iuz_3jKdmYen6!r5w7DAKgGgK^oAEoxqVWlATW2cNiP zL+l>(pCAHEMTfF8XCkg3{0{`#m%Z)RQFEEY7wfp-F_}Bb>L!_mV$@Mvf%rW0Edaj6 zy5O&;BujO{u%_+|!fMjt`$I>)^MCc@_AB-CkJdi0tUa7m4l~I9HEjBiG(r}*L3!x0 z*~E+dpO!d3HXG3st|0}xcCUZHJqi-Re&8`!c+cmJ<9)7p_d~w>)K}?3izLbWP@*ft z08RR?sX|n)@Ji}nwJeBPbD6U`HTsCXHNN;XusQ>%i2D_W@6TX zhjtss(2--zdtm>~iypDx8&`a2<$)I&TAbyxOK%xGul?q6`Wx434!>fX=CWXZ@^1Oa zm~EoKkHQ&?J~slbCmQ6ff=o-0u?LbGKv%QkpLnW_q0cb9*7o6_{iixA7!@#_1Nt{g z#b3(R!i1uy4hv^20%0$A@A4vNId7Lil?v(=P#*#Z%*@=}PvD=cb-^zLU!k$vWDXz zRfbh)p-!_rXrmvF+qVNGu~ZPPo%rrcii5Qaa-0ne41}+ZC=hfAw}&meN|lL$UyU&H z5|17)8;3UpqHwveZZ^J+w-7R#Y8c(3bQA$x?+9vmLE#N1(f1Dz`yJ!rQ-R5EOEb3I zBP=`pC*z;rj$OT)5=+Xa@|WHO9$|=~%eqzja#|%^|0$%1!LrbuzXS-VhFEscc{t7Q z&6bF#}>KWDp*Tk9h9-_3Ky}y=lUKEF# z*p?#cUG$kz+>6#QW-M=x@AVk?S^u=cw)H(`GvJyk0`4+2}+T*Usq!naH^dyuJvAzzIOBCC!E#y`s*?&VGpXKF= zX-pGtp?kWiIhmw}$1J0m7FfP|sdAro=iw{Ym~*3dW)?pxlqFVV-S57IJn7F(rt-qN zae4L}p-KGCSYngC_}fSs);Aab@KC915lPnhw)ft}bZ7=L8_UQfBu{B;B#nzf*#Chd zI5IMl2EO-+{RD2eMG#&R4;@yv#gPoIp*!ICdj{kuYy(4GM+bBUx$ar?_!5QN*;TSX zLOcjR%Qh>Hu`~T7&a~ta1iS>1U0`PEN>yA;$hN~$rUeQk$MMo8_Q+9+AWFc)0nMW- z&5;)2Us@^KLsdnIdiF1Z>T;x1W^8;r*@p)a`Ns9U_Yd!)!1P4#6MAK4%b=E7<;6+g zzqX5YZU*hl0+os_tFFJjPhO7?4~z9yIN&+>&^5l}IGDWDs9OnaaX{8Z0|&t$aJ2^y zvU0HssDVN77qhf+5Z5+4iZ+wAP@pdXO^*MN6t$T$|r zS&2sX`n5~e&P1ZHy#os$pn^gEpJ6q)Ap8MWxxKeK8q`x+>YrFZBH!D;3ix~Dpg{@B zeWldDi7%FQBUy*M5`1~eZl#6{B^Pa zS^zhKiJ7^(gy92yJ`0Af4$A!lRurgCyEA{;o^_71dEB%N)EgE_MOy~o2s*JU1jtc? zCc#LoP?7OAi1`q)#JC3CogAUYlM`EDN8CR;RLcLtKV&U7ofHISAz)pEW_JKqN%=ln z>RHA?{}g^mUoC}aCX!_ZPU)jU$PfvkV^i%4S@hWjuogH$m=wmXxvh_ZSLzFH&<94T zeAZ10jpATuN>$;21INL%T8Pu}(yS6w_!ZDey;-@+!&3NpjUZmc9=|Iv=Ej~ntswt2 z`OGaq#_By^7WL>;SzazSU;`VdIi$TZh&;o%a(+%)ApC4mFg`A)RIKvvP%Ae7a|E}} zPBEBtesg}n4+0$%@HCDnZnBsA+%v47x zp(Ed0)54$ZV;r4)KsJV13OpDP37AT>G|4JqyUB;iTrHq0vl&AI@cJq+x??hE{`J^> zPteA4;&J)^0tv#gvwyv36RN7$NW`-EYpruVBod1*@X@+_35gc2ra_*rXp18hL?gCa zB*ZsT*H;$@K14gj-%y~+Y_kr`FXc7Ax+0&dVfrz+WDvk@l^0!mDoGjd$5s7v-lM=k zifv?+s+f=#_oS$RN-CguwU@atd!@NgOQK-R49t%u{e$ReA+ohJOebe}-dQJVkN014jkYb=8F+R?K-(g&@ov)^Bk>XvR0=PH8UK0-* z@6of}ybdKyKT%SSMGym^Bde?ZtvzBJNm$65ICQJ z;`Y{&0l-ymftT~BS|Cz`i6vX^^{u@KWnAg&+A@r70wEijdV^2!&kzu-46BRYAxSB_+b-={G8-2& zXxM;r05I8QBPTp%zWt^d(+z-CJjSz3s-#-1wEH{HQX}*@BnbL*Db2oYk;X zn5jbY-2Hekp>U?mn+n&TyIBY#@@zzRsI|U!wpMRa=OVDAVgn_Z=xBh7zUgQw_-G-YCSgMvn`XKA~$ zBacVsdW7MHg?ao66MPGQvdSRA^Znl?Q$2ix3G?(`wCoa>Bo1#%mmJm5UmfvA z3cO6T6is;A#Drb${ITl*4x0tb`aweL!2cj3`U^+jVdip&2?k42Vn9Q%#6uh4!W2sC zOj+ftZ#|4cB8^Z|9vNlza_tjD8wX|=#<^9Nl`(&m?&Q?eP{(u-83>G;fGa5~B?%yRU{QcszVo*UDbTKei$l-MEL5QbF@r!_e52|} zP!t`0x#=_fpa++(19V5w@F~&(-|j8f5PZuoA?GHaa7=WpVz!^h;aSML!mC&yKXs)4 zXHxqg55@73MUuBf=YXH$)RFA9WekJaL=CFzh?FPYDYyO z>TOn6K9P*Gg2<;bM$~c7c)NnU0VaS~xTQs^T(d7xKvH^3QoZD7nMc8NJWCWY9-xhF z7%aW6%KDd*x_>5mgB>-UTXALb_SlAqY>2K6$ zb~^(~t@#q=1BG4fMO-;VNonpyL^|_D^O80Cp|elo`$due<%$xxc<4{=oan07 z+zM6p2(O_g&u`niWsdMam-wx%oZ4GZ3Q2)!$J)-7{)ocP$?d@pQkTmyb^9`i7lPLb znafk<-!o+dm2W@gc2VQqci73n&c4Wq?KI{_bk)^gDAO~c^hA4>i%%Bm-@9mg$Erz) zNDv+v{Y$J-MGy>NvEdF>pB9BCu7I!pd0|ECL53wiIr(lrsRTx82gW*xZjdw@JMv$w z4BhL*mrwi&*>{fgYskqh%zj@CWk!z7{C_sd>W9ib17JMq8GMhwvq(+7yrXfY{_T5Y zXHh?D=iRRAY*r*4?)`uM0Svwy--pDyl@O*a(qghHCeMn1$HjV@rjND=aN z8F7dw+66`fjs-Q(MiP>Q+S(9K_P6P;c%oPrvVSxIzg$RdV8GwVy~=3_J9orm=|7@; zsTGO!kB65)$c%>{Tm<|d-}i```hf6?$`JquN*#;Dx`Wy!!{-MV#k8^tr9p{ zb;-@=&j{F4ujyEj&!9-8_pXp2O?F4AbhODV=1bX1Og=$6reH?;iQjFu=B%=CEZUGbN=>`fDNhLLjX+(#-ulruJX~eR2M&}^~ zk%Jr~1?+WpRS-4j^RvsEP>^Gu0GM}l1V}){f53q86#mS6X&S%b_W|#vtnwaQ{=*5p zQ@Xb)q4{He_dz1LGN_H`m{mDOpL+jTb$Jl-Z({Q1qV(24p8E zHSM6d#-N$ApWAxWnPTrdOht(H(qJX#4Hd5oQjA1@X)WDl_unGbla$`uPzAy!|MsL4 zcKVEW$sHk|I_iPax6r5M?d7fW&vz>rT81D3_AN-_&7HhdTL{)MCR_ohG)_Gp)3u9! z8f3v?D{mx}!Ju-hBO(4!O)l@45ZRc$S!j18rYMm`5HARm%3GRFF=%2POO&fEYv|-z z%G)hwX>q&ZYT}5YRQ&Qdzlt>M3y%aV;OXKB|?jrXOq?A`-42g-{qLlC4~sm`dWz4y?Brh z({rA2^vX!1pUaB`qGf9GJJbHG%heqGg@wgM_$-veQ_KDBeNa4|3wD?lVDA;FSl#Ky zK?|(W?n@yeA_y=4%&R!)7_XXb|1>MIn|QIwwM3lAT{#vR2b`3svS z{A<=g#|$j`lpnzhXV?_uwZBBW`8*QYIpmNvrEyEa$EApTaPzB{e6fQrn1x)5gb@i^ zjx_{fT|Z)h;sq?et*rzTmw*8K)J^gP@w(IbgaZJ32#^UFI0#DcW$J2?E%V{G1u!He zE+EL40V>7iWeRz7+OjJ%)56|@9h$Re-ezV0CKzs$q9&l2D=^6Ck z=-?o0z}H$*Q`lqlid7CHqsD;*Nv2~8(@AQkH{*F?dg7D^j1(%NgcyM??U)zK@dGkq z`MEVP6hy+L+SUWY-!o_sL@KkX9uuW9I$K9f&AR)Oz*Gy>v#3V5&^(nv@sJm*g68VC z!uZpJ(9`#tp>Ps9kQ)10AG7O!qu89YY*LQPqm*Ad<#Q)2Z)o&GmJ-@%a$vY_r`a|T z=mK>LAH>)|e{IOylHM=F?PzGrVqXrrP4n5l#n;2JJ3l^mIo~LFO?`Q9XG(HeCQfdl ziR&b9;Uh15Y4=Akh}v2WZy%AnNm*?5F+krGMs0y2pKdml?siisNk0pz0j+EObCr(})8sEw zcCIdZv(Fhm?Y$KScVl|TCgyjq6YG9(8i}j7?VHq5%`fX04eco>#CFU~tW^|3q0qt~ z0IWYxW);S({QXS=Ot!xJJZQHv0`1CV8e?gybD26}o-f!QRItJGgEqUDZuE~G6I6~x|I@@5N@V*b)0+Ad~@*B4_7}smt zUkS!YrPNmrvm&z97?^bFm61i?bEWXlzFT{VE|k~XU7AVKuyW;U;TnrWTkZSvlh!i- z*Lml+HTEDbIlM2-TE`4#z-l#pU#hLQL`v+OLT4!#>BYxKVDCI?R7R6Ipd)|MULTOe z$u?2(m~Q)^&n7?KOK2;RKDs-3goqi{`jbHG=G)3?mI6oO?z&AV%wjW9YnR2#qhvx~ z5X+Rkt+Z2xmf`DED**Zo&Q@t@X?Z`cY>Xe__*mgj9XQjh9H|716?U|@%jQlET4%O$ zKrg&OM9#Y5mX?>dfOFaVu7_mzjaSwLY?_Lg+40Gbd8jOV{mt{VZt@S_n?hZ!IHCpg zyaR?PNvWw(nRt$_(leYera0IDFZvgNb(UZ}$3{mLidEjC%!^@z8YjYd zUe1#1!Ye6CXG#fD&D4}C*f12%Hp3@!YaxOO1jO{*jSPhkb_#Y6b^`y|ph)p0GEatz zjp1va?#$9mvt{GE<08)~IC%fwgF}T;5oNp#a#cebui&WentfNu*~@&WG%_iTDapW4 z1*L(y+T?04ba_1p!5xQmYV1fwQ7oG?WzI(PcT2rc&1uHAk#mZ|2z8m`(wf0WJ&SmHTTpi<@jv zI99LQ?>>|XuO;4$oop7#LXI=7Lf(CqOCLB<0oNSTk~?5z-24wZpORi<<$Vs7uRQtVQBqhz z$i(R*`2F%RcDAI)tw7Wl=~Wg-4BL!G3j%dbme?cYv!}m2lM28fq_LOUjw|)z7Jsvx zX;xJM)ke(pDfFk4yDs^toMK(V*T`x91>CK{F!{EWh+*#&j;fju_FrPdc1Za67) zNYQvEaJKuak>V2rx^XD&+OMPwc7r+tM=cCRdVeXB~Ug2}?3~ z-6Eo1&l~Z?5zq*={{D!{_&bFGw`Q|85~El>?P`-SyFUpZ;Y9`E*>%}&AraoVt%ZOarAa=< zHUxE{_j)_y-xJA9;g3k+GikK)l@!|61<+CsxA7Cj2qY-pH-^1T%;7j#I||kp+p|m( zy}s9fG%`#(CaQznG_8GR#M__ZR*+19}iZ}(O$&HSbBQz3q>L=Zf<*zO@iTZ9NMCg1L zhmT4c1E0R)1+R1vtsSUm#PTjC!;+87doX3y7c5!lIF-6B5dRb*3p0$ShBMbHx9Klo z4mLwkg#4yvfZN?3j5%;ii#kfPORevTj9rK|g91@(wNkbA^8gm}Dc+LY*wQ!X;xBE6 zpKr)PihpzS#}4W37|ZxX^R45=-BS76^KuHdDv15y9@oc5V^<3| zxUy^I$S7JWsnA$E2(=2Pi(7L%{?|k+LX6Spn7W+E27?Zc(HxdZHC{0)G8DttmDd6Eo|;-=mM)GfuPpg+*-rDapUgHo9{O`8;vpA_>i2 z|Kze!nH9;)v9dQt{rvGYS){sSsrmA;W?k2WCKg(Pz^4@%aVV55N|<@7kJI?82oIbWoGDREFgv zIuvYVBoH*)_ z-&Wh5VRK)7aAgmQI4!Pk>UjnNR1~YcHY4#9P%{v>viGZkJ5H~4@m>Xd4D9UHLFC&4 zXk-7;zQdNIgasS2o*U!uGT#=8r)N5H-XY}+k`T-^`dgqT`~E@F+XN-b5dK0V{n6s`Q@pf^x5=VjLW0o# z5aOYIk5Tn5v+_+coyWXl*S6O5pty*cAwnD^*v@X9RgWf)RMAMS6J zRa6sk8}Ze;xIN=H$g3 z@5S4>ULAt7BVH$@(8Ugubv)v(k`~^7#K*SR(Q%gPs{A*(hWw zY^&waWl{$>6u+c?(Wi)4LjQ^k$DiYy$%daJBh|QO=;9G_omN`c(73p+O&$_-XO5Wh z$yYj{uCTKAlJg8xOmEt^;s&|WrZ9ae{$kp5n4W;P(k5T~RN~W&1c6LyY`>@sKI_pf zlg0N+SfMp?G~4>UAzvA?X_E1Or@w5=@}>YWoE&mE;}=V!`p*hJG1 zN^nG-Xf*5?*w{;TB9!bMfA4{l77As^P!-h}f=gBH)4aDS9dQ^t1SB<8@1;k_7Ub1P z@)^I!ZI`WCUqcAeu3ok0yz3_=EgK5n1SmIB{W+UX;z9g;zRK}a`bBuO#kML^wIKnthHO7i*K$PlY=YydW36CEuDrl8xB3FIeS_PeHvHc zE1)Mt+ckCUI>qxu3=QNsL>Uv~4zcKeT@|8f9}=Vi-t!EKcY7FG?C;I$wgh9Qf+KU14*YVRwUGfF?g*2xB41ARwtUr`cR?`#t)H*`k z$acqgC_;Geef(r&?m0&6+?bM?)OAKs>XS-OWbT%2;>W8ab?zS`B9m_}u|;%hHc{~5 zia*K|q92h78y}nA-ctL#D}EriRv>Buj*sow!Bp9)Jr^&JV<(q@VcucP30?Tn7nO-? zF7ln~{Y!w4{Kkp$yo1?-&&9&d36Zf+pOPFtG-Qypfl$3R>HJ6LOTH*avb^_qt%nj7 z^7PiXs_&>)h`%t?r^)IzJO8L`cMh1~zeLpYzM%v;z|zIFzm2w`eD_J8#&RJ9?P)dx zEHZ~;YtdFMe*AK)m6LDg*&8AnS@;UZ=(ubDbpZ84^q>2liihorq;Xxsp`DG*lr@J% zy{5H|&71W#f`C(9Q_~P^mcS+pdQo7&VqCT*kZk{UF~DT+3x z=C!ytT-vg@BZ@4FBjeua}gcVjR=RtFBgG#l6KK- zr-_a0HfMNKY=es9#m9tkwrL$;gaKi&ITebf^}$-KiaYQ)I|q-pCFElt`8<>IITtsU zqqVlcI+aa2)y|(y^GOG9y2kgfW@BX23h6yk@J;1`Y7 zv&*r2D~EJ=b`oxCOnkX;1v0x9)L6r3tqNq6715B74uy?0Fk4(&q#yS94f~~Q?D5&- z9B(CEq$(GTKn-$z?GnUsKXg*tfn$E!@3dz8F5u&dgNssL`(r*; zsX1KkBla+UdHx(pCK}CssIcXJFVie6niyliH%@j=Pl0M2bZ9aj$^W+CcS2-W?YUMd znO%ggT2Ul6ih9{5gjRWZLuld8JXq0Z9C2rea0><~W|Z^eSs6B%Ke!0U1VUhnIK=X} zt=SvPRr|pXyJS}X!0NajMa06WRtQT0!F{Kqn8KLb)Xe_~nEmb|{wsHt%C5pt8^IMV zTr*^mvB1nC_DsjU^>C-!d`XFh+eHL#ZdE6ud?nQCU`t5)Bv0fUbpRmd}?%VbD7BmEeb+4PvmQNV??~ zMgAIrqlL*2yAID3LC+gD`F8ine4-YG{G;IXqB; zpaGV5lWn?+n+aKIg-RlgoqUndlA+O{zLT7}eTx0+iH{zsipOH0%6LvfVE zxWJg)H^$ODy`Flug><5<(w&4!vnLZA!m>`E$wA>SaCBgbqcE^?it}w83vm2h9kiw2 zW7LtOUgY^ng&%d6A9bD*W-lga0z9R;Mgju^{3rOgT+Am9hT3nxZUeVEB%{0vp=J2l z2rdS_1HR&f@ii#?&Ax|38^dyU=lE+t(0ZATmkDReN1Tjp@39d-F@%swpc4T}tOxJ0 z_kxa-Mtdt4-1aCG^K}<-mG?78mYcj}^T5k`CIJ%KSP-B7Nc)V^No^$QTL`%RfY7tgI>Pqr$FCA)Z2>O?YuU3;3&M`SfH2N+M#d>NO0weK6prp zOXQg}DH~ERRvEr?Fg6-$Ve32T*pLl5*cYe|?sdMxhhzFWqHFwV6ZY;bSn$nE%u1cV zKyE}?pm+Bhr>1XnU!=?AM=C(%0K^%X>?z@hC@092OOUVd>6b=7>L4Jny{_LZa%-aejxNQ zZt*e!#yUwWHB8M=lOaj0NP%HRAgcq8vVgb5TLCw%JXXhN@4Bl5>aSh#g&V|7VWw{? ztJ!#Tc<(VAYBF|Mo@B3xWSpk{!=ko-C8m639$#x}%@n6hBId$Ms$GS7;sdeod-OYj z9xk$w-gmn90zH42(n#$FoI41v%peLp2xd5uWWs#Im4f;tV*))nj9{^wniia=za+$c z>~MrZ9i_vjM!EhNp1X8NRmbrdUJMaTh3<@*n+r`unaq_v6UpuNd22ZpF5?{{b%=SR z=2cFz@W(%vEg=RyC4`lAY&Z=V<92=8ws_!vnZ&8&dp!3aqn>>8I!9YwhGQ{hms3L- zS8dA0mZuaaM>rO=g0BnhA5E>JXi>fMU-j{x8}a3^wV@wsljAGn=x+&z>% zoI^bIJ%ir3-ABlw*g2T_MuUYEFD+KFy1Lca@c_BzD^sprz!~1$)FxSqbH03?_HTH{)*z24Fp1+=Mfi{hH zr!T*9=7z1vEH@nT*NW3!zstRF;t4%7N8*Kht4k$@a`q7RS1!%C#UNHJjnQx{O6O@~ z$=M&QYM;5johO&l?a~=)aqD4RX7-gB`i&Xstr_{P?dkXIZK`+usg5S!CJUw(iHr;; z#6#lZ+jS~pphZKQ#f1A8`pn+!3%paMd9h#nuDf#cu zxxeT(#Mge2NEv8CVl*xeVn<^>aDD5FIrr~r-smxc2)5>C!FF6c5uAlz{C4SK%Dpuk z&L#OP{cQ0YmW^nRfu7Em0QM76LR;ANzLxS&BKG!hKb#`Lt6nmetE~MH=XI>Y7PO<6 zE+VHr4+R9in>oIq;=pE0G<4YNUfXfM)2WV_Q#7w%NrEIJl}Pgebi>6bYpWl`rWjfW z+Kke-JE?H&JN1um6Hw@{*B}dW{q{!OFp>*6GKx+gpTNKyQ!pAyhe<{g5gx($7Lwi_ zB=BJ6&4-h9;<|*%H71{lTmScqM{mz?7Qeh0I+TXl8o9UE*aX_2!G&!BT=9>s8nkVs zlWQM;otR52Af|_<}RgjNM~ChP7I-@axrB)UP;?Fzl;KFH*&q_)@S3^ z0YC~a1JJzvO>2khtvGE6XOy{vFFHS6_4`{8OPpRMaiYq+vb3M;AvzM>N5qONy?%CK zH{)Qhw9a!ja(87nsi)TXKFIE2==xjQr*iUF@}$kse`6ZWO}QO@Hbdz0R1k1n@~u%3M5XLO1;W%8J>~ znetBx80IW5cv|lZ+~~x;-0~dGFW7zcHW~D5eeYz-x^tAc8@7P%du#Z6uaZjppSkYR zwsoII%pmW$@MO>H%jg@S*j8Qc2%5XuK2h0os@)lMm_1f-I}`Yh{)ah`alAmF(fP6PM|^WT*o8HjztJqf2CC< z3^+DhSy=&d`5Eq82)+-6eyO6tsEXxk=S~ol5fRb{6e*zis}&r1ZHbSS9EZra`(X$2 z!okFnbzOw(E*~=?DhL_)q8NWoV#8mpBQwe-d*IS~$75PTA^h-*&8|=#1I}(>wELSO zRLTbRO)PH#iJ>Sh#xE#6CFow0&eljIcc(5njd!@}dz=#j0|`eN9j#`zD*>%5SV)+c zk6%I$PHZ-Mq+&ule)9;Q(%w4woh}PHcC?Ij@Zwl3cnUjg)D4KT62n=Ac1e)fBSi=z z?Nr{D{hg6K@@&_IWkKRttJZ?%#_F_-3&2IvHTrj@QUmdzvjJ{XoWXPi_Ix8VADc6`PhZwLM?l1`xcsA)q}_Xlr|aw| zItTe-2-){tiRR|0239m0vJF8wsJI0zw0{{nn%xuq@GQ(+YN5n0SL_07oFXLn94$)R z+FP}RchP7l51*tIzn|Tb_vqu!*fyMR69bN)IBWibJT9_Z5}T~K3i8hPe&yDGgh6ED z9u%6tr;)=ax&obD23^Y*_X#+H-&^%*lFY-^@JsZyHeyL0ErBE6i8PYb%BN>nflvnA z*MIa@F*B_m;mtn;m43eWYPM=xPrS^Xt(s@GzG6><8WY!>k=-<%us6_hs-?b>!AYfU zR~LDuD9$G7vu^Ox)>)&N3o?jx^sz}hAuUi#-2C%wFe=_UFp~6b(cGoEdTQ(9f{H%P zM#$lKmrBq3`nsvE zs@VkRWKOTth=3!#6I}3L4PmxM{qisw-rHP|)O91|RA1h_h#9hOIWZYZDfK`)&*kNM8fKY2R9h|2O;6Y#pI?)0y-SdM!NEq{ZycxQy z$8%55tD7?aFP_p(uyN91FW&-EJnxU;*9IslHwyBLvXyEKD0A|0?#9IQcrs4go0cSA z0iG{w#TB-AOk)8P-y<4vck`0(Z=3owCYA>_E+TL6@b()z1C}AG@n{;dmIE%+qb`jqV z+h~@C@nLAMzbD72h&l|@?|F-Be|YpjpiEj6dXU@CJIWclrKr_rS5Xm-QmSgLq%*l5 zapgr8eM>0>Y+eu^y$3j>Dy0m0yTnk>l^@z+WNx$Cc^*)Y30WH+;~|Gg_$4Uxoehme zP}^c)J7#QN3HQfOuh*z|l<1blUjGTKhd#`x2o)=z4}=sfL^U<9o8eO8=7z+^ciZQE zNqtdRP3Y}#3}(jv+~D>bw@hkEZdgpOZCF%3Im%s>AY5+BV%S*n_ZDc~gsH|D2d4D= zWU8qCZdgrq^P?6kYVt>M?9qZaAsco{&hFYbjNNZl=tTYjK6(&AznPE(Xvlr&;JxdZ z0|O?$l*{DPe1Mbs`3`1w_~)oqC+;0|hM|7Ip=T|9L*5#(8H4Vh{tFftL6ZXHs4B_d?J9pwq;CTt11QfzkYnirxS zC;I0@kdrmBnwO8iU~1aqs&6(zow>d%T4~(#I^h}kK9(NOb=vheuh=$|*NwAk)ggLV z>Lep~z;%wcapRZXl|lBFgZJv5zHIiD+pHXUyetYSelHfoAzs^z#)Rgo;)=zfyrKP$;F;W$c#SfY~nLfZv zoJTqC_&BGeXQW#Bqx+o+dZ>VkcaM8C3)YuWoR6sUY(RXEWWy9QNv`aAp_2)uouM4# z-4$bZn#h~*x|P-*Xr=Zc_$cGB&@|w63zwdHbvK#q@5cB*SO%;P|D3F4j#hzg<1ygf z^+oa=1a;_O7#g`}ohTw<&6xCsAViTH5nrlCeKn=b3po{L#$@8*)!plT(TE(Cchomc ze`dw_Bopz+fQyZ{``l+{RBo2Lfk|wW_Mzv$2({pyYgskB3PlMxLb?GyCIfft@x5~o za7aOQoa%Q0ndczcT0Dt zgtT;X=tFnsJN(zXmfyr9$}`W*?0fHP2N95n_w!%P;!I;0(}#*1d&$bUr=?(srw+sx z)f2$Olpy2uwkcs|+E>v%`c%=e(#XlU5|z@U&42@-Yxq0;<~%%PN(zP5%d~95-Mh`mk4e~=`mI$bjgSy)BigGhJ~ZrPWZ*MvX5UvIn^aC2d=jEwqMor zL$a(q@D6^Ye}cRBFi}0zCNFeUk&^yVAQcMdWY-ov~WL?H46(Q-A`MXuvafZ@n7} z)2Se_9&I#|y>ZBq%8!kJ8f5!^%8_EYW)Hecy5?k`~C*@h*2HL~)!~Le}sc}au zf8^O&amZi1@sDL!ujiZ%)N7$;vv_|QZJ&Ps)5+wqs1S%;4q37H9^?`7+8~6i0*;9> z3tlIQ&h3_>WLoAR->95TDtb{6iAzLS(EeKBtC-~K{*iWl@~d%gKM}n3m{J5IzT2~!w@AWzIyoRwv zUm-r6yp_zb1w(idmGuSU-yWVPdVl%e!ERd!M&zuw7yJH9aix&^#ZQuY?gka^8pryyd&vbl7bs?}H@e$?Qmr*S&I@j=L$e{;YK z1|6UVY{27u&OH`l-z|Ue1`ROwCoSo_G=>Ed6d1o3BOzhcR#1%!QB!%6q+S=l!#Vumh?QB1Vc(ciy~L5U$9|3&$wGv7 z8v(gN?PeLKkSULs%D05*WP1;8+2i(69}j32D;G_hgQggOSc&*zs0QK^hL)BLe5w_* zF6qZl<+J-BlMaMA2@y&RCvO8k<5)P45cv42!;&h_&kHP5EYc`{l=HR%KIgpqtik%v z;7jcL7&Jw4{FuNHV~nr6)P1-+z17Phy>8_=glL?eRD&>bjAFy8o>$YBI=9N5-_sV{ z^Ar<5ndeaiej+mlhq>BV+kFLZ#{4Kd_!Ux=3v<$zfoNzWMmW3T+JH!{Y+SD zt~#1at9s~sEVIAoZ<&-#j$iiCT%b(H#3`+sD4FV<={y%NbT{@+qu9T+wy>WaTQ?$0Y=^wjdJ;%HH-r>KG+e7)kjQKD* z9>FtkiiBiD^(dA1^k>S}ntltbFbG_KP07bK3B;Jggg-p0-);9A5B@CBKbmXvJP9Bj zUU|OudA`1#Y0x3C^KMoOdOk1U+Iyg&$eOxLoP{gu`-Qq9C7JS7@^0+;RtTz;d8f>Y z*UgfrSp2(QjVJRh<}?=^S_K^;19bS}fs@G4P$sZdO~_>ueb98Z1}~B2nYF zf2S-JO>t7;vuJz$ski0mr)Z6RyFDE?Dzmn~&z%Lyh)(CtrlWBM8xdMlQ?tl&SXna( zOem#T{;Cp+*d<9XEs(WtJPJitYM8UIg9)lJg9mXfh3)0?zPGEe(ha%t(}&ojyy0W1 zGTXsqy!}Ir!IkVdC}02kNCG*D7BSR@iTYu$1j)?MFRFDPX65>yHY^=xOyv(hbcBUA z!FRv7To3PLbXwZ`e&(lQTqo00q!Do~vF%oM9I-0MPnBZp5|(s39h&&XD$?KJ|VZQFLV(~#UkDp2(^pT%sBIjl5IW` zJZjR%!$g1yuulCCZqM(7e5}Iow^1l<%l8Ja9?@a(GiIVOy^}IesFl$3FIes`Zf1EQm+1-PMZJS-`*@YA9$Kvx#EvbX+g5F z(BE)*o^HZ+rrSP4#Wh-ezxWisHM9^MaTB&r+u`Oc!570`BYT*U7|W|;9NyF$(tm13 z-Q)sAu?%E(Vf>}b8?9NpTjtS50=Mt0MwiIrw5M#WwAcQVH4vnaK)C@vT2OMWuKMa; zM#!W0?NbKCyA8Z3pMv{6o@~@eOP2huOfQ;0`Bt48?!RE z!@AiUux{O6sHge20^wEvp1l`hmgFUFxO%Qe_H?iE2TSOR?O}rXQSs{O z`GoXI^y#tVncEg~>^`_x(<6Mmd&6t^DSWo+Jgf6Erj#OYdy$6}z9~Eu)YBVcAk$xVKl1MEbDPjh5RYX zW!N<1Tl6LjHFYn4mVsZrjn%_P+wVTh?cw&vulPtdj+R@~(z6!dEgbNDtK2$pUFJi) zy6JmW-aIUq(I4}>ht(>T44wbVS@2;E9sH~XbvzC^z>@~vfp~@>bBlri2g=pV%>gXG zSo+Tzm9!Dt?&~JsMJki9J&Foh%{2`A2}?v_)kk`+1SH$jzsEVW+YqRqR4bV`qJ-9B6T*Wj1~bkT_h_?KOT19 zP7tcQet^!uEJF51o;Qu-0l#MT?#hEsO`=#zN9~I9-Hy%>5>qfw4eO}UjMMSxmsv}% zw~h~C7+fgRlDk(I9S0T6gE7e`7J68uIsSZR3*TxW%rCuUH{rr!;>2RID%7$zXW~*~ z;==mHn-Y;>T#{KmFE_tah=$vKe0}8H~3{R%!0PU zS_(U8D-v&q2a$2fV2o~?_|3>W#H{720~&bSkwhyHZtKHzK`uPQ3mKZ}mg@EVz>~|iM zmGb8a0!n+I>z4VzEg6XjIXxNjaP2MhAklDK@J0H==~E_rNOQrSoQVG-IN)_pH*?0a zoAX2*|MZ}8Ahu5hV+D337FKFjgH2?z|L<*smWf|h1%=LDz$6KlquW-u%G1qljpxSn zSALfxpena_CuRus`#kr3^}J1fr{3)8SNuJl+Jwyep>a$+DyY@PrxRR7-D$$|AK0o* zcux55PQOSj8`GwAaQZ~GWfWFBzg=y62PirqH9Iuh@#OUD*RSf{*tmb2lpO6NecQ@6 zx57)Zj6$58eK$AU0Q{;he+0TK02d5U1C2nV1!z(R+EEffd4nNi@qnZ!JPvbysUj@I z5d7xj{k6ktC(H|HFaSU>7FV9oj=WCZqGdJ)-AxgRZeYEQc7<2$2u2%}FO5YoLZJF? zX)P2ayV4)iLb`fVjL7Y{=dDSyx;k*rV|(U}mf;@Vlv_w6%ak>=LDYKxLGZ%RCn)M+ zIdh8`@~q!?MH(l87W3&NQOJL7Sjc!J=3)le>96YpVLvM|68Hz1@ROupx;l<-RPo$; zIjRnl*DtYcu_*;QGmO69hYMl&AT|+qWiaCU_9!|2$16DogjT~(L^~uYF;lrVndprk zgND`YA0bCzn=~pvsx8>N@ETL6fOmS{C6xye_v*xP_7$F`a zg}|$dhms?@67ASMN~vdm*74t=aQ=2#2`7R^5#i4>t0s{8d+Hi!Fd0SAGV%ghrG-OI zfRG01H4vJv5o`p&IB;2LkC@-h`+4+~>&TbYINQ!Qjut#e>-Ii{*@X;-sXIXTc&Y8L z_VEIQ@nb+txSSgnsam`uz(Ah8%x_5Vt4KG3ymx|b1~MNcUlusPqJq*%(0l(8bZLV% zdEkHETj$cQ$068qmoy!n0EZ;5a>h;<@howrZUaRDPsr{| zVH=>H;AT8e10CN;%DGsR(%Hc_+qJDil_1UDJM zm%LrszsVFbYy+|xr+SFnE@3)y1{zJGzK=p>5oUm*C}V&&jAz{c`Ow1|2|HAu0qi_G zXnIbB<)z)zsjmX5k5q_~V(^O0==w7St&#`=43R^>Q1;`M^4|6#wAy|;-Q%z`w&T>b zBLcw)$+#cxrb)##1Y(&PBMLwxH-zU_oqdt{`em_`D%@Qq-l^j16{92#x||+BPZ{Q# zmuD5p`BN=3WYTOHLr`fOp+rzfZrg6cqE;|TH8>y^R9~hXr zoHPk#I%#@-x_>x2e&B@a@(oo&aLkxeCxVX57vX09J*Rug?9{bHV8H%kp>n?9(hBqAe+*))SlmE$eog3Mu zGY*#)_b$f?lJeu(J1!xdR@{RN{6Ry<{hr{xh2XS&6$g83bIgPsPEmRhdpUWr&U*E~O*j%Q0X`*p@T5t~3gBY$F5 z?eFhc#5IU6a|J=`o%x}+Ls39T8*{}{U&y%AG{-9%+z4gz$gVowLfdoF74;(4?9s$G^CF^;%?9PkTF1P1xUJ<735XG7j8Z2 zMrL4SG&<(^PfB3SkOfcvk|T(a#6{*t8MY~E@)eG!q@$g8*;f;b2=k@QM%4Oais#7i zI@UHp2D<~l{#y{W>^hjcZtrn)@fyigG`PcM8G)I6@xE;hCNQ8vY5v%CUf7OQ%D>et z;vfvFCjLq{U`16Y$vc%br`^CZjl}#-`R5xBmHE1U$2ewx0rHTo9sDT7w`Ml`UDq0q>!M`u z(q`muZ!=<@M!m*n1auOW;qZ`)aleVFg{oFh_0xnz{(9a}^E$c$EKVllmSu$8+ou4S zs9^-Wttxg5EKqW(UP`dS6L%J4m@QqPy5O%w-mDx{9UG@>5~-J;ojcJro)dlFcsRb{ zfs;?MvQlLi()A6Q-Qw^A9SO@CdXrE0$+axb&0Gj9_)N@#K?t$SgwlUwEnap`cD{-c z!~3^)CeZnh?!yjNYu&FRu-}pWRRTjV26WLG^6?Pjib5iDR~i&QN&ItzzCS;btMl$s zizV>N=R-#dl&_MQ=&8a2>ENWy5)Brz@m${I;C5C16q@Thw#mKI{I}fW$ilF9>o@(HV4*W7Zr!sR$sI3d!wNz9Sm1>`(<_t2JU9{v>*VG(3cm`))b#?XZ$zA!9 zX|R)SF4S4{%vdbKZyjIXOG`_?U_;I8G!U%QcKzsisgbVAme&g8~x638BU-iwkWK|X&6Ei4vb0T~pu`MQ9J=+&Od2`9UR}~|XEqb}SuJ#R zL8@|+?R(6i6<6j#8S|+B`=A}bwyU$$|=Zt;FV>jA( zsq<)&!sgNDUj0L%-5q;6JcW67do;24BhKn2L*;i0q_ z@jPC!tohDgKk9~bh))Z|6TeOTsqNgT7WM2rF16xp^T``>ztRtq4VMMJ#SY%~1Xv9$ zXJu|b;U2BMRe15Jgt%Xhy3q{XbK&;^y%*&qU&tlvy)+))xp%Rb2;i zsleW0TIQ0|p>rxwd@u{V=!{05)a_J%V~br12EUfh`A$Y$B9&Y^Wb*;zRZTNvmQ$`7 zx1F$^eMiyRAH9CrX(f=hVfN|Ya2UHqi6t{MH2k5T#9HiJGvN1ojTm{I*eBcUrh7uR zw56pV^*AFa09B!dX3>NA+aio^cX$eCj6YnN%MB4RmKVK*i?1e)BwN&RNf{~+>!)z6M1dH_2wd0-ttmX8q@>wkTZsv^%Qyy2jfX<7572K` zv!nm<)-6Ydmm$j9o2@=L52{sAdzvtQ=GR`N}~cxlf-#HZ^+yxaC% zFLF6i#<=w>c}3*sQH!_BQ@P*Dzh|-6vsw}V{mA3Hu6(uGZk91dhkQWfJI0KHpt_Yg z$39D$N=#>qa5cL0cj1eHD6w0H_;+&nMhjwzg%Zg2_43ip4k%h}e*#BEXfvnXA@9DF@yb)z_o5Bj?|{Hig~2?EqcqrYZBD$NP-hCl-;! zRqx_$%Z`gT)a9m5_nrfYHpvI>T>W(PsObK_&MhN(v(ONpKj_3r$<u!Ed-e;%$UA}#5r28inVFdFl zyI(iO%eU6oA1-iNh+zu;SH@gtF$=h+3yhtPU`X%Eoi*y4k|}>;bI5|gqM;Z=M_Hbi zJT>CYU`TN==uYZRn=ECcE*&y}PaXf~{;LW^X=-X}(CCWaNRfDa1Mud_ zs494Y4e$kMQw3(`ZD$oZ>w*X)uEb~~UE}aD>Hk4Tn1J~3)BX)Sa-|~U>g5H8sWVE= zMyLn+v9Bt%-#NeO^;*;U$*_9V5EnA`R}~;r`TlU(b(C2DaE{X}qC#X>j}SPUvY^3B zuuT^#dYO*Gtz&&Qi4e1{-cN0ng+q8(RYW?J{d%RNj(W+@dTYz$(4X(Xhwb9-I-2b& z$c?z~*yjhQ^IbQvP&u|2__`I(3xL>z13jvSy0JmOB9&mD$Xd@Ln+8Pt#;Ew+F z^@0g%QssM-mFzO*w}kp#cmoHQgMr(~YLpQN@15ve&8F>NxyZS{L=fA}Sjp$$5D#<0 zk4=-EnKRQw#B-lVychL+*nIw1TiL=svRCrs>+T_fW~X{H4o0(R(-X<>U+KI1s0b z#TdAP=b`7bj*}@RIZ5o)C9m_#VK@~o7ySL`tEi`Oq4h%Vy}}i27N^Je=+eZm+KE{h zHK5<@AIEualb8JE`l5-rY+#f$|3-W}$7R9!=m9$kd0d8oLmE8;GxITQfn+QTCnth1 z0V*qR7gF)rV;`PqYypMc@XgI^I%f+gF^anwl4PUJsQLX%r|3>+{85RLiW5 z_V{@x*+=lyyrW|c$s*kvtS&Y(APf%E|)QH0lfw5hL^@>N%o#=e*aDu zRT*wxW?CY8%A#MN{Wa)R;oM)2t0b=pV1#8V(#55p*=cn) zd^a|{XWB=3Ufr))`(%V0%`nwNSw?`o9au-UL%}znU;FlGJ<KVocVYaI?;rH}r|_&A7iRcBl2-HhI)BHbr$b|h1*XBt2*uxi42m=EDLpi3 zoUq1&I-F+se7oEICad@UWiA=vg;|7Vzj$d6-;k)&u;^R>ONzY~8UF*#bA|4W$;*Ml z($eyzY0N89?y1k_Sx`XXk0EFS2SwCz6p{tw$d(uW!1g9|gZS(VI6g|X zNbovQU;NC#rRh?zys<;uqq*JiKxF-e@ADAow+mE*iZig;m=w_E#;QYj%VlmwSK2z6 z^fd5b3KYzJiIIFP?|U^nFBtXi5C)cDrG+kw4;P_* z0YCyh1>E`W9tDBk*}08IS|*w?(2RES4qm!$Ciz^P{Xp>|NDrk3i^XXA3X4Os4v)HW z-akQR2TdQsevs;ai-cD}>*<4-dUJ)2isiulY@Q=R?c}pBfo6_)7<%GvKjcK(@(TZr1zn4VyqUSxFns-BeVKIm1B@c z;-|}vtG3TqwPK#fYP4%P8D_BS{QJM?<~X!Eu&&0aXS#PSU~PA#dJVjf<2 zY)P(M3dr=T>}&tcefqeT52I^2cIx82_dF=~&?EP9kpXW5O~@Ut(DS*_*fhP`+`O2BA!;EZx&Q z{ed3`kL@cm=47X3P=I-zS@#|EU;D`K+*fk=R9{Ktj*wmT77F#1u8x+*zZ(;_L~gq2 z*Hex~DynPg*jQv}%D5Sk8=3a}SOs*Ak3xy{zlfq7DJ4N>VN_eQKKmx^`_f!eY7DB& zCWRv<_zNX%8yXteUb#}sCm#2sNgUSs)VgMfw=9EqG$=dNC^J4+Eu8#N9sAgsV#Bi* zdVJqwCk@cOaHKb{gzj>M20{2bV}w$+?I^E5_Z%A+Oyau44jz1L-gB2&# zK4;Vy?rjG=xno~rf854yx7~Ys-|UoiDif{|rg|yNC&CrsD9{N(n23D5-L1@7G}7IMns_Mp;vekzVWGux~wy^mC_t#)5G6S7+}?nf|&v9OkmmU{-J?txnM)?rVO_bPv73k zJS%g5)}L8B?~m$4N~hZpeN&jWPlMkD@^x@49uj1@9kdM+t?Tpq!MM6gjGG%7%SmCm zrBX3=S8|*%baLw0Td8VV_4Q9;Nl~KyT1^?_Fk1An8-8v^+BdARV?=otzlrd0G*$Me zzIVkGeoknec^9&r-Q2I$o2z_6`vc~YX05mGhp=Q4hTNfZvn^#k8ewiEf$Z-cDa&-u zGg%5z#%Mc(D|J=w#LmCPzr|EN&Q%q6Vr>g*2KIhDsOa)b$qUsk?d(bc3#xa+ zLGU=~Jc)+jZo#DJKi96vi+&kbGo~fw6lAQt$@pTCZgDjHm5x_R`XV)ir_ZiM-S=oe zq_OqOQf-};9G)FJXa4<@M49#%Z`(P)+uD%a%j)(xbxUOa?mP_M0@6{u3 z(r@Z8E0kiTUvuhM$9J2{O@^$U{v@mO&-)_Kk$!fdnCbA237}&DQAf|%I1m&C3SL!3 z>HFq`2&ML-UbHDYhJ_u~0to?-yK55Xyuas9$3_fZvEA=`uxOrgPskIICeB4%%OV_< z++vFk$x`t3;>f%&fJ2+5Z26Wa7Ro1aXhVi( z7VtXf^VUZb$q-B)o1{Sl<7i>7nA+G1Yg1X90u(G7HA<$UuvJ!#!5OS2w`mX6%Z$yB zVq!B)>?4#^cKj{oH4!|OA@I5ZUB2EOnSLLkR!14xb-4Ws7Uu*S9xDAlHflr|j*CaL zl6=c{4a{#S&a+Iq9H@jVvq4soaqe43JBOI_` zToZwIRect`Eb;ZaW)o^jk=$3}eV($?ci3g;aXp#88t3Ggsnb@Z-K;tEn=Gnr6@X!E zYhl%ZZR=s**!xb$(c3;vckkfUZ@3&6+|ou>_n}*}#GVVEN8M*NK;MzW6jSKKKI1F^ z+atDB8oRwOi_i^G=Yb>dk_*I(h|ie;8J!KR*cS;uvE#uAG`~!T&#ROeyelqSb2gaf z0?lV-syvQ67}pRVAwXVw>CFJ!Cujux7|5l8D2`^jB5AOv8VTs+?9{AI(ypLqx<|@- zdG9ttN;;QjFex78G<>z;R5iO%4l@j zOB>tEX|Sc@?XzlU4;4=_BiSi@Jm#f7YRlSq5LT{YdH9S8_8>Fu>FUEhDU zHOv#O)M)WZOddUwgNpRq38*8YHG)IhBQwH^R-B=N&<3o?D38yyTF0g&ndq*@l-WBW zhgJLU`iw6pj0;r;{k%HT^1c!7d^XtUkOEC-$*5N3X~1w1#u~yRUUh)=?+$J&t~O&MHg$RsszoBT7C%~q z6zko)duKx3fPbm7SaJW7SCmjDA6TOCP~~Q0uMDPWNpc;KLK~$Svoheme9>fID$ywqZu0%g@fBO)?$I&)6$8Q=stDAq2tp1H`X|ZTp>4~NRa$b#yZJ> zdU{-v+ap3i8cwT?_{QG%58kJ@gY$aE$2-2kU4Op+-g@wiwxVmC`Sq}CNEuZuCF~Gg zJv|!_ciMV=C?ocKNRI zz9L)7XD&JENRG8Le495paQSG_?iq90e6$&HwSlS}$HEyc;@D#N5SJ_|=+uMT;4Jlq zt_w{rlqcGq@8u$1b4!LUp>6l-478&?M0A{TIZHkqeeU~@?|)m72T;O5#cGBgMfS+y zUPj^gqer?&p$%?7D*)qfKptCs`avbw?F*Xm?4V}M7*mm}qPhM*e_!CnHtyzzD=?ht z$ulL!8MV%T=*ZktNYc`Cw}xTfTd&B39z(QO|0ao#Ckh_zM3sN`yb>A4B7Ma7zD@Mu zS$xK2@j^~oft7x~6?%qx?kQ!GNyb{%-MM#Sw-AgRnXCO=woU{Bp)VG-EDIlBD*VLL zRYS*!TF2c>fmj49mF8Q}#IlW{Y3SO}T&_&_vJwDv`W@t&2dsobxy*ubQw)t3{42cH zO}rAgf(3#Num=Jn1LRnV2K&WE035M!iANe`9AGf8NXg2G8)k!-)QrKq)Gcipi1S%R zNQjnIb{}UyTQ~ZMC%lKRgUvUAQLLs!LQ(8@*L#$faZ?r^o);GBvbi15s0ivpi`EYZ zbr3?Us`$1vDJAJ%aVJ)P3?=J+-*cVLJ4`(u*p($GAaTlqQX5xKqew|_AxBLSK!Rva zl3*)7h{z`_yhY_Rs~K4An;Mbb?J7tI)r+R0)ge$gL#tn_}@uD zCvVy<-unnceqvU2_R>pX55K&{8oD;+LW(~yar)iuzl)Nt4R$kPJ9qvlD}F)9z_eZs znHuKtag&-VVcNh5e%-nW715Y~qNGz3C2PQbZ}O)iPoqj;N^930<@B`mmCy-NKs2|c zyj^CpNVla}l>m#DB_RSfjmq~}@E@hs;hsD%5?;|m&LKmtp^9rg0 zFa5G{rCQuNvjeGL@i$1!nGxPbumepoeU3%@3XdYt`E?ZI56^q!K`s0%HA*BnVT?d$ zKf`qAQpAU!Bf$~vJ(NzEya#MJzA;Gi;eBUImx=P`)VRf!-KLCY?k*L)D=d`7eb=uU z2n_EtV$k%%yFk=Rc65JH2g;*x&KTeE<>t-bNBp%o7g%QVsA&0H$iq`7?>)DUf13+C z=Fx!Pu)oR(A4wRF#Aj769lC^%f@P&v&ooWVhn_>}?(j{@t>)-idr~FSu*lLDbp&7* z3$XDe(*}Bz54?KYP-*xH_TFnRK(6Nj*(XGFlHrU;_wV6rU?BX`IRrKrgrjU*T>Z}uSwtVa5BYkY2EFV z9L7K2O!!*t;;|tk7J0m#E<=8}(f;6o6O**Xaw<8cJzwD}bsDWUfLeVuhcHzS!6Mak<)yJa%ejPQ)(^)H{?0o3m?KT<*COn$1^9^;z^|np*6>b?s}tsDEZv1!&?=;hG8o zE7C*Te4I1PXvBHlE}I@14{}{IFxv2;CteXyB*R&g9sfwi#usapP3&LNTyVKs8vI=}78td%@IDUJr$?iS zA56A_Ok7$J73aSmtelXfz0cu{Y$N;B^d(;-7bEauHj!I3BAH&ZxV)?oLA51iD?`7T z;NKo~I1WyY(a)KaDJhZCwL?{d6vTp5I8evLs`)mneQ7!z2j1~s%FcWuMR2aasot)2 zVeaQpL`DG){x*H6^Xn1;SJ(DV2YS(!7UtCuv68+1-%ByUxK*4en15{AanC2*rPvmb zuD7RB`lKpjX~<*@VwI-~M@uP%VtAzt_N%N$bZc2(dq>0cYA#<|Q}#wS3K4v9_QPAf z)d?JoNVq6u0>`xIAO7zP2EBa?Mey% zdmX<1LpjXL8!vFmoNPcYHiGOpGT4PYDc

L14;n5Gm)+FLF z)hRwr(!T}F8Li%pcC$=ULChn3)iYrJZEw_A4T*YUc)rtp-nV@Ip8)BO%DFq0})&H;N{u`)NfQ;A2Bj3i})!KB;MEmU1s z7x6DuEkLiqOah1NLB>%^D;?wz5;m2Qt#LqMB}~vtuX)$2@Livud~*_Bu_!|R z^}QcS>_ku->qy<`y7g+f8Yce8GZQZ`+n3S0H1Uw=fW(C;*jLbp;_pin%B>OMHrwhN zq6-$)hUKs^kvvW4h7?|4>9UqlLG-#ur2qyCM=r=1HnQdM%gTXG@op+@@ z)tPuXQy6!!UxxgOj@c_&|6%j@@&PyyI`MzE8zrm&YC`eLil$bdE~~fAf)eB2gxb{c zID-JmX4BUzFWWr-Z-BBiDH;d1nnnsbG2O{#yD!rI2O`aLBrA%@gD?vuA6v0WDY)>% z^9-m_*VFx@;G<5Wyg0jv73J;V&%hCi2Dgv5XjadvJbIP=lnv1yYKHl#QpMVO(PiEPo$q@*AMOm>oia;iM0gIT1Owt>IxvoSb_T6pK)Kq5E2r;7`iG)OOZu9w+6Moq`<=f4F?eM(nZul1v-5!f>!*d$fDgm9RIA)^;%VLP+K`+tVx=(?YV zrfzzn+WGc~dcW=Wu&>h|1h4tUBVn30!1{M@LsOlu1O2r?5FG*aHK1NaxkTl~&ed_t z^-R?w2M&@3Y?qA}-h2TD4dy{h=jT?x8L-$jl&(0$NbAYzk?OF_+g`au{fQ!?XGuWL z4KR|}#8b>b2w7XI-{uaulW3udF0n$| zZ8$8r-63`ZQ|Ex?0ti!@qA(ItZmxJ}Giz23g6Oixui0ES_tFpxtO@?}V89_Wm7mDF8~N)siI=vofU}+#-pX)o3R8e^|PtonJ$GJK$dH;6f9_7kyQ-3bUIq- z(Y%Hg?t*#O^KUjV2$OJd`Oxs7sVV`3X-Z(F4Pzn{rJ}UZ5)Jg6zB|d^M>^I(afNo5 zb+U8V=I7tu?0zT_&8$@Jy?(UJ$xKR$mgL`4n$|u_^-rmT8TH|s9r@BBS8o#q=W(zQ z7yrood}OUj&Nb0=y5W78tzElM(y(ip+j7l2c1Z9@)p#7cNHr_Sy{iA&zMcbnGUhba zzzuKW3+^X7qFh8PG!?}w70L(Pxk~+d*;~8Kz7tbzJTtgmHca~xV*FnoU&sjXG82k~ zwTetS88=W}UIBJBsij0de{tGw^u5#d`q(RZaVW7kIMR7{$YH!@Cx{4t*P@Fkl^!ju z-oK$!A)YO-XJ|-{#Zp#UY70EbAm*Gd8wC2yQ=iXtpO7K9qb@5LEZW-IMpVJvJRdZo z6KAoWj?v?}Ihj(>ElDL%iUd~rVU(inp4B)acU=I}8i$GQ9n&7LQEollo;$W^gS>Y; zYRnq1c@YQ0_mT-=ij`L1_T6T>NN5di{+UTo9s3a)GBm<`;t7)o)#JR z{QancY1=j^%R1$c0-Q4-uCpan@NxM>5bhjtRej(j!UJ@i3Wt@b?8^*NBb^vjT^x<+ zx~gQ3G`hBrm`wk*On|!7`L=;+X5F!IuUkVms>0Ir1Wk@0eTg=mvGo>J%ZG`wGQhaM zQm?#3D%+cYaV^YY{ub~Qi;AdJ+f|{R${T&S(qt;?fS-%cjQ4VwD7TBk+908(#s(;> zCB7{-H5^ScqhvXR;do#ChOxSM#ZZ536;gAWNDl1(5xA7n(oVQJj{vo^=yk07g|LiW z(Y9UDnqA|E4#4Iice=d~UUn%uvMVa*)>0#~0;yvUKz0(-1<^||M?&1sAR<*tO9GUg z^N+haR{~2OdT|7E?#C?O<~N8&#Hokap%ih%HP>v|Ebo247-oEUFq6O8e4lK zw1ryWXlOw*FKtVhRY95kKY4WaOJS_Slw8!P9OkY&6FQPhe}m}YJZpxj;dplOK5T$y zzc&XI%FNr}{URfVReM$67PWEBNsv{diesnRGVyRNNJ2)~d@Prs21-i*VF&Gz^!FV; zE(utW-0yiiKf`M-wJkdkz@rEJ(B)k^ImNa_?Pa>op7%Xpq`kK|TMqTJva!G&4?%gC z8d&&C=|LpKK!M_q#=hlR>EsVPp>0=f2f)T}%`?U3-xEW!Q(`V+Sdw9kKlHSNP9c*C z3_c%TEh~m9_8lDWoF3fSUPE%B%jMxsCXI)D2?80%Qcvk;_g|h#=m#D8ma1}UWyaM| z>MjOk!(X3svFy0;C`IzUP^#xof;zJpM;hbdcHzo#{@$+w2fy{Ndj9 z^DI{4^|JEYDn7N=d*bbNrFUUsii861kCj%pVGu3PJ8d0xpOzjV4fN$q4fNLDXJuC9 zapAneds`E+&0d4<%isy?fKb0pa`d?Qb@U2USG!e*4IN;9%a>WlFD{WYTJ zj{IMAqW^AJyP^Rt2Czf1&p88l^q}f0ndipN6+8gLuH>=4aVpQ#%c=GteE` z0OM6^duRvv2!S|3!x_nbHx@`_Eqt1&M@H4KICN#CSB$tI)_uxtrDZY2QlxtSyT>vb z9~RwBXb==u(DwbNXi$EckSoikD55C%R+D>*SLp%e+jZ>8_xj-6?Y?#FQ7Kfau?8bp z@sGJ&oM{pbU1S6;Nz#?8<;=!_m|1`SHU-TSc@2zBD)I|`;C}!;Pm<(wH%F!yXn6lN z97zT$_s8vHGa-aC?cC8*a<4KV7WLw9FP6rLtqlzhU!x}R!G3UERi z!ggIx+cv3Eb?BvRz@yEkf0Cu0Q*0F-Aa#KrtAq;1`*Z1p`hEUHv%I!2-W)zNlrZa& zxhRl}J4j~#o8r_km!hkOtVbdc4oz9VfPso+-XUO#%__B-(-}gcTm6^|`SUJ|S6z{F ztAa|#2DQzx<+gJitr4S{_+Kk=+sdO`0Cdx=0(w|?EGOD$E|{yuF!i_Zi&F|fABEIr z#|g7xp4iyS4<>w~3lk5as#dBtX*%21d9EOSyd{h1*o-fr1+5F#4XK9?Sv`270B-5w zk?P!%Ue6^;;i_|k0u1&9PE&bEOKI+u*%=_UuO(QBv5dbdeOs`g)oReJiQ zOVRZ6)Av87%jE(~bcDz^nq{iEPv2SazE4>7+b-FFwFW*`wlEH6xl1qFt@w+=cNGpy zG)c=~3b^r+cH^d!`N*Dj=?IBuOaR{;;IEmOW*x zTmAm8q>Nz+UF$qssbgJF<}i$uQ8M`9rraB=tu&a78NRNi*0B6w70&Z?vut~r!Pj6*Y5E!A1oV$S!QhM97eFM7#+`ZD;mf6b+3#JVut!s?jN z+jlhmj-L;e=;;d!1w@W(={qemvkYCCrilpy4L$1m2)}J}KBMAXvoE}<42ZOr;s{nK zRm6*tCWj0MJ+!69R;$(asn_`Vi|N}(agcVU`LOsyo0ib!{+*|XQ!TrN5N%4Im7U85 z+9S6HucI9|XMQ!fUO-NYLX{*l)}e_p#XX~9@mPY6{L`6DC_7`5K^+XHI+BGA?Ks-p z{sqn{U~DkC`_H<8Z7mMDW#)idYk|ntxJ+E=SJh6EoH>Q&g86La()fIXi%h%PP@!tk znOa0U;qn2aUP>-FJN!yaGy`XW_g7_5hpKPG%Ilk|G421y(piSJ)wEq3N^vU`in~j3 zcXxLy?hxFycah?5#a&8~;K3>G#WlFQ6t{2p`QGE;S3<&OvS%`LU2C1IQG9B?ssF#_ zj@d00lzlk{=|%%9b92h%+&GQb<@VVtukMragtjvFaKHzcS-MmYg8(-Sv!$OJSvhK) z5CE}=B}JnRX3w}^d3jj?6XKAQIW$M1tf^5X zpGt8d#c?wm8)_0{Hx%sSIz(m^rFZOrBBD>9m{g;8Ow<|hc~94v6u^sMJ`^#rJ9MP{ z;UvglMu^G^>|Lr#NuSs33=lS}D4H5j!;t!{hb*PsGLPp!pv$ZAx+C)&Qk4>9er~0> z5lMHi$%`!#OtaTm6$-qlDIEK0M&24HY5j!=c-)s~uM03_Ri&oPtDWgKsR$I5=^Ybv zdT?*P1^5n?O$-RtC;cH-FbwUKo9A+=s(R>UVt;!rp?KT%=;PJJ=0V)rQNOS#NL48YBPfRYxOT@pXgMDDuIZ741Q_My9=irGnW4B{RtL@(9Gy><;N%E!=imKkV-8d@Bwz3Cf7|PF)?!^p>g-Ip$TNw(uX4vClI#V{j0h{*UEY*{Ej_C@& z;OnhiwLi|>^>cOW&byPdCXT-3@Xn5~su{UHt@1xOHX zJ$QP8z;B+8RC|#?WK9P>3u*q7CruFeDuAMK{X9KIuex5j8h16hRN1aSVscjodA%9Z z6IbeNc@P1|4TI-Bic;pu$G5l~i2f_)<_Z8YY!Wdsxs0y~X_sjutGQ3T^Zyd|5d~ z1^JNKjAoR*VTJrj9D!Y%4Oeu-FzPU7g*(8w=+GZo2xKk2l35@Bj4{HaoDUZrcWa(> z_7rL+S2VR)(zISVd+hm-C+5wqQ@3+g)%aD4SyMh{(_U~_W95&!Rg`8lP^iy_DnORf z@vAua@h2Q<+gt5yk0?t8dUbYOt1K4rPviu{D>lb;Ml_ki_nT3eD;YZLDPTPOTRxfx zWe~DBmI-xL^F#l(h|s%Lz$=m{3oi55tNT;Y6hGsjP;kY1$uAEtY6H!{YK4-!jPWdN+{FSaPp9n zI5=A%ChYX0+B7_l>SP4tJ`_Wk6-TV?es2^^wVPj5Gs>;1?a933Lu^ZYDy2fEue0FC ze71~tNc}<%RB^AheHV##8-q!P>w_r%hZ7nAZabjO`$K*O5FLttP(#IiawxQNb&Uh7 zdmD0!Q^X_hJ3KK}*@qF&_G)>O3YeC*;{9vu)u0!1qHDk}#cBU}) z8}a0FoQ|WD6`lH+;=vU;cMbMrNSAtr@vD!ig=4G#K3($j&4W#>$kTX&Auc1+jq*$i zfKTAz;n6cOu>qs^%` zuwHR8H&AqQ&Ah#Spm^cQ%FNMR8}CH;C)%peRRhyR9_GHPKpY;P7BEdk2rYX7>!WnO z6y*{zp^kre8U};%m}}$qRE%4a3}#ekVHQ5%Y*q=R)ZUO;_!GJ7Td+^f9%aldeyO=T z!%o2zIJ~;E1oNPRG8b7u!gTTO0?zz1HovO;V)7pW3Qr43UiETvsmiZrjHX|+i+^nl zhb}1!U%b-@AYzm!?q2mp7+zoFr7#04r6?GCB(hNuv^ehly!`_tW{qG@Fa= zds?vb#k8sKOD_;s;9)&ZXUM0NNor=Y!$C9-uCp)7AJY8 zo>=ad=m_F;< zLM*`H$Dna{-jBIolp*tp$R>s>r#hBCc9Pd83L_3H3@g1kUL>^GB^!{rlzrzBcHOeY z%q75ovYU!7|Fqn^)X|XW98^RV9frE1K}QFBOqcZAtHQ$P%zES|j#hz2BP7}Wlh8#i zqal@rDudm2N!(u6C^A4O);qGN^f$4f(g2XhSF`B)-Dn^g4hzZn&j8T=bD2AVz2~cNN>~s%k6JdP!UskcN0?PKkfXWYG zbv3A(|9sXo1{^LHntqM_H>m1avdy*xH)7%%7 zjRPhU5JA&kkIS|KVQM8=wNzn?5l zK+9n1@fcphHpt)W4VbfNeJ?_oIzXRKh?0DL`d;S>7Zo?Y0^6rJLFTI^=Fe~psi>Jg zE%~cff0^yXEe3i0!!4KaT)qaS6r3BiiR-C*g$ahG>vyV3=tuC78XFD9+*0ej!wFQ7 z!dPTdAt8)_^dVoR;nW^i`ct&~luwV^B{wdsbzN~ep7ggJtwbvmc6DsQQ9p4kzi^CEzT0g#g=aWvZ^|an&&l9!CEZBAK1d#8?BGjAhHg;jVJ4nF9Hx$^{nL|8Jt@4= zkyC`=r0>7-kQ{+#93Py33`WJQc6AQ^?tNlS`vPV*lzD{4jq}+_@`sbb=qUSqhVqmj zQ5h~s8FiXb_)C}BB?UJSWWf?*Mq`sIv>!h zjXK+lqWs~wP+*|>B+D+kKse$r-A;dAL@l^1d)ByZX6CdtDrB+OPT$fGKJPm1;U|Ge z#7cNrXD1VG4+^_g{P4_CObknRdgZp{q;>=cdAc9 zLZ|8rAcJ(1E%0H=$B(2pMp|5|Jpk+wD4lSMr}xBit_r5P6mME7jpp1fwoeM~2@NO% z54&gEQxlKnl>Lgj$YlVur#PBseeb`V{Y&78kAm+TX?g*8|7~nW zA;4JS$lLpdO>JzY~gZ_9RTZ zM2G#9k}cm20NNDH5FZcAmaekVs3HsO0f(md(c1BW+IAL*#;|*pQJNn~>l=_W6@_^4 z*tF|Rp=+Z#g8EwTS9A}vUud1P?r1z>>AHt>1ufQJ&J6CCy$Gsmn!tQD4H|?zWcR?A zCgI2r@F!$5QLOW;W0J)8WuLe}nCPs|#i6-!;PlolB^$@pwaU-qdum&b#`Y-rF&YqO zPV9t*Xfp^Rub&`>KgluMfiFskc?lDYK%>UEmnHoY{B&azVBDX*MQ~N?vqx?PZ~ZN` z#fVA0z`&L)Iso&BGJ)iw(V)VcPUZt4n)Ao$@Y1{?@X+%6@40-^EZ)*3S#%{hVQj@l z)jRJA&E4Cl($y>&NO}D!X}D#^f~&o0b{6%9Ed(_j(WK0CY+Xf+j_?7Lxoyb-tw#Dn zQOEtQ@y>l-)0FBfWpw3e`OAL!=kh1dl?qQ&PxzHL=&kWzndLzYBK=TwX^o!=(tGQC z#BM-A8IWr4ycc4l!NPu*73a~AGFr0onD*>h*8J`y-ZJPB=#&kIq2e8^oL~XR3N1%d zUs*sHTl~VK&00$A!$mc2K4%dytGJ+yb}Jgvhwg@rz98<}b&JV+Y58>0$lIEE(`i}# zBO_ysIO&Q@pk++%YoME}GFZbL4|~M7n8*U@+gJO>sC{2R)KEzOyZLel=v6ctfONkG z-Wh>t+83&g&6Lpj=06ygJqs@BjtqgAP=5~9WVk(DQSTdgSe+NhtgVwL!+1#ux23uj zmB|Uo4<1f%>-;8>#;V|Uc0rr^tnrOpjG%YQZ$#9uB?UG`$I;f{dm;GS*XcrZow%y9 zCXMnvwa!4A4_}Rk5>jWmxjA5)DP!5U#OCOcEZmy{CMrt#am3V^hC&U2fje5(Uy*O- z$B|dEEpeDr!`h3%O-&{AG>4e5hWDaJ+UQ?c&BbfaVPYq;fH2-i5EJ12IoU1t)f--% z7t`7q=uTvZ9iLdDX)|G*bd(9N4s_!Gz)52MZ>L-n(Pao!) zn(RWdnyJA2!t9%yu)u=+ySr=;6(+aFM_(^pp2^Nb!{kFZY0011KCGWRy;Abox)NC& zH(^-}Ci+nj|LHQ|=;Y|oxsI@yS3^&vq2TBCtBOI2dAb)|2%wVhu7AN{4I$dCcq_-s zRpjZbKo%924u}BSBO*rV^X6{E3wK@{t<;E+ejPSHjhz-?Z@HKwns$a#=bSIVlbz*V z`I@T}7i9Ow!16o+7n`-v4k0yh>txzXEssl~G8jicjrM z4n5$;PeL@TN^U9^J$O4|P@-Rwq1VBXvLlC{fu&*NRlYw^6WahIAJ;Coi=pXo_uJ&5 zN~FDZ5_5SrPdU`7D5X%RQoV96qL3an7=MM?r`E8Ay>*cwC%7{!Wx0=Mf|}_~sZrKZ zd!g$K6hPEP#)beAO2C#m*TC2?a`G`cX#f~D`_qw z*E)|G0(cLc2Ep#v!X*gr$=-Sq*+JpB znA7({-r|#(ypDg|+XC6{j=vR>m5;7JIe4|P#2krz`R3G2^}|OKSB){fzUSI+=c_HL zP;2DKQd8@eBPfdJpuJA~RfE({`bBUQQiI-!$XIS!U*&qP<-^ZV@Bj|`K5Z*VN_TXd+~LR0m#M?I?GCX z?yCm+dhU}ADcjqmS(3fWVu_E^|ELl}*RXfOS!K)h2$z6C&R+YImO+_P`o^%dC>#!a zB}xsRlmM}xj=h~|blplpGZg{LcS>R67mu@`vvvh~*d6tEe5vruxi=@Cdoa6VR{9x3 zV0N2rS`7zj{tGV~hq}qXRU_?$XYWP5&-WkGot57c>*dl93&RP{lK5ZZyjM_1%hyqh zF$9d#0;dhp=i=o!VPbLd>3zSt|1(0Y)ug&%x{q?B?^%!};uzgoB-BwbLRt0xT5p(X z%-nPH#GsTOD70bPYtedw^>{V*$EtB;Zc>vnkoj@ClP2ratTHqGLBETcpw&-iAyWra zK7liLNX3szKOlg0sn7D4uxmepY|VQ?TGU0GHWPi@p=SEWq@Y>;b&rpUY}J3cfDctg zjdLr}XlEmhu0rRak+y3-EzDRws+g7baFrbGvhlAUj6GAX zhtVj~g4m{{b_dkqPqcvpQKY$u;6$%QV!4zMNYvIvMhu?X6$Y4(FcuG&j`o?^a*iBV zeuIr$DII83z6<}dzJ!m@gOI640DRV4NZ^jNJIEB4Sa8cG5O*|asefjOV%fSvi2n%Q z+$p|T(IAg*_>=C?i~!Ifsq64rhZ_rO^v>*o)45+cA9r=}`QnL5$9rSw2f-B}2xM>| zP(qbmPMRcM10VO#f(UISA2)-TU|mxcP;FwG&c;uM6mBTyE#gOLwc60s#gEi^Uqyot&Av8~rl!m8Pnd_@JM9-Y;0k+K>Nw>w;RMc6~?71TN; zr&`m_4H?-COXc!&wyu+&dpe2S5Vr+E&6ZAOukEpI1Rk9Hws~YSVq0OK3v$uRkD@(* zs@eCjBprx#Tc(M@hwlP<_CITWG!Yh-G;RL(BO$Oc$7i?`+xU3zTR@BjFAvFN$Lh3W zIXlMs(~t=S=Ben$6Gl78n}m>JaEcc=KNkJSGxj^A`PX50*?LzK|89l@i<4$tYPg7> zYgL%QrFK(&a&}q=bo#p1s7ia9$zALZEoY=9o&=#*irGQxCk+T0r%N2oSX{(wFjJv{CX^Y z9k8n>bqz)(fo%$@yOJU607zNNwJc^O>CL~8J~^Uo(8skbLQ8?&sjGMC^Y~mGr3O4B zu6ot(`U)}c?(@a!4pUQX(8R<{{cY75DZELBQ;+w}s`>+> z0*q_EWgUw_n=GPj-Qx737?Gp1wK=vWK+F>9 zJ`1Er4$x~*O%D#&EykZ$D!yGi3hu2)uK6PF6p2x&Fy(5O`QL`EuxGDeADWtTijt6A zLsqB9w5aPfY3SHpCzTlD%v#d6Y8a_8@D8J=42fH01j&vnDt=y7Rj^??A|126#q0p#K&$FD-}abSHw_Jx z+!Tw8{p}}yx1M_E)N3Z41zY{@r`$ayr6c%5r4iVfT)w?kXKqD_~GJeiafIWitZT1`YR3&}wRc%wrp$f@3J2~Ze1#mxs(|`MHi|ld_Ms~$GElul7I0>CnRf-O_tFu#DDM1 zdGz!C3D_BL_pIc^{fY)I>vs0bpgW^IBK37YET{MCI-C#_YKqZ|)|sEx@&3eUHBm*S z+l~AF-rCIN&oQ(f4o+EnUBZQD_8(|<0*n5o21+o-Yac4&z)1_2N%8DRY@bJbbZ&FJ zkJ8~v_;u_~zV&pp$$*8qfTkw_tDlqsu@%N#V08ocj8*)D0!GH(GRQ{hw85}bm>A+` z;$&}-xF|hf@U6jFHWSWM-rJyddV2PRPaPe*WmcgdVdA4$?xIi3t{XvE@j^}|Uusq? z`B$wW`N*SYtsH@Mbjk!tEU53R#9M;MgP_kAE9OT?bop6OjQ(?`flafIK z&VhF8(V}1AZ!E5N_IksdY05iSkt{~k4J~>~&;bp0ni3pXr@;bUV6)nFT>zKyC;USi zsnhKfKuNWoBkRVFZ+d z?T<&5KsGDZdsyIEpgA-K<}n$3HsWe^Zn&pXcfJbVzk}!5Yw@21?2Z#a#sQ#;4@ClI5x7{8VyIv3&*X>!^ocwfpw8?>}TVKRhob-eZ3 zOy;)#`(&Bvb7F>l*AQU!7M~cHeIb0>z3!SgUO!cwNwIHib*NpBEY=)u{2%DBLW_}$ z5M7}}!z-?+Cx3EaJd4-~KQ11*-aE!f32`@PH#o$I;kfM*JmqX^k>@y{JLG|KuJ%cv0WHM4$B|c3!I$i!*djKYkb|?F4$bP$ zB|302HO8;jYj;qeTn@>$3J6Ws&2+S6weOwkoiXt5%_**2hygO~!AYsVr+!Kdd;+9N z#hK0H(aOcgU#gL>)~6F!btjng=KS-e1bo4E_t0y}y~_>su`O9hd}io?iJ>)i@a&HU)RmaorT( z5FDcuVkE46=Ifm~aMvoEYkcmLE0eeRSjuC*^(T&cRTJoz8p26Vh1`*SVB7u_xic_G zf7^Vc7nS$XRNOIofZ`tWnf3HIU0wJckmNi zLzIqFsO8yZ)}W2?ZFx-<(v9;zBfXeXxQENS1s8B+>(`get_8gD<$oa*n3L}XKejN%v0I_p-`yUF$oh(-xi2$vl>-Wl4M&cz=Y(#CshfH(^U&-&RpV{&!5>NpFR#Jj8B zS?Lw-%gra|YsQ{8MgwACfu?QWA8Thq>TqyUAua)FPHOudB4OKL#>nev^d-bEASE4a`Tzy7~=O)3l!9O!;JHI#FukpH4;JOHrHtsfU8 z!On?&ifH><>=3h}H9|6RPE;c7^#b{0GV6Y=TmsUm)v;uwPa!i+Q6-dBN+H+6KK|SG zMOXK`R`mSm`Hd%1+QpTJiiPA0IKU|G*D>)YoNWwFvuwcItGx5vrn@2ImH<v|&d&_nRGw_~j)%)!Q;3-|FioQ~P+v5(8oNZKBOL@mW zzGQ-#QTeDPDakvXy(FhkWQ$|os*aB9jvL?Q+uj}1XfY9w3OiZnY%|3)C`?03Y|=sM zKC$xl=6wDfM8#kI@>+G4yXk!9CXWHBYq+vMKawq6kBc$l0|%0r3}Pe-T?*5}66#T?Vz1Of&GrvT>XanM8&(@zRxFS_i6`^Aa6^y0g{eNXeAF)L4H{hh= zxCLNvP{Q_Bab0SP1clBd+S|j>5^O4aD|jaV$)-9OIR&pimB0bS1jzHO%|6pV}t z;rja0i4$(9>77}6y0pBuFBuQ{U|_=lkjVyp1(aABIPJCaZrvd1itn2Lef2jZa&**T zUOpt13I*iE#f~1h*N=6DeQmKPK&v~3oj!e3PhTJQk3sUh?lROS7>$h6fq zU0*=Z>^sAk%+$muOkF7!TD16diqcG{9z+E(zmTD8t!^1+ka2$4Zjg4&Y?GIBb zdE8FRUrNe+$5NZoCl#zUen6sVz2OqmW$YOXydDuRC63R2AC4M&RgfXu6xxoiTlNsz zAQ!a@7r%`)5q*YV>Ym^PXaxTuz#ilNmNS1j*FK3Nh|i4#8#Goj4iOt7H*bX^8jsG8 zJGcm(+QaI^`BW6!_Y&3@6POi#Fssj9q5!drxr+$Nd=#7 rCl$;t}FJmyIUS&)X* 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 exampleSubsystem.getVelocity() == RPM.of(3000)); - launching.switchTo(idle).when(() -> !driver.RIGHT_BUMPER.getAsBoolean()); - prepping.switchTo(idle).when(() -> !driver.RIGHT_BUMPER.getAsBoolean()); - - // driver.createYButton().onTrue(exampleSubsystem.setVelocityControlMotorReference(() -> 3500)) - // .onFalse(exampleSubsystem.setVelocityControlMotorReference(() -> 0)); - // driver.createXButton().onTrue(exampleSubsystem.setVelocityControlMotorReference(() -> 2000)) - // .onFalse(exampleSubsystem.setVelocityControlMotorReference(() -> 0)); - // driver.createAButton().whileTrue(exampleSubsystem.setAngularMotorReference(() -> 90)) - // .whileFalse(exampleSubsystem.setAngularMotorReference(() -> 0)); - // driver.createBButton().whileTrue(((YAGSLSwerveDrivetrain)drivetrain).driveToPose(new - // Pose2d(8, 4, new Rotation2d()))); - // driver.createAButton().whileTrue(exampleSubsystem.setElevatorHeight(() -> Meters.of(0.5))); - // driver.createBButton().whileTrue(exampleSubsystem.setElevatorHeight(() -> Meters.of(2))); - // driver - // .createXButton() - // .whileTrue(exampleSubsystem.setArmAngle(() -> Degrees.of(90))) - // .whileFalse(exampleSubsystem.setArmAngle(() -> Degrees.of(0))); - // driver - // .createYButton() - // .whileTrue(exampleSubsystem.setPivotAngle(() -> Degrees.of(90))) - // .whileFalse(exampleSubsystem.setPivotAngle(() -> Degrees.of(0))); - // driver.createRightBumper().whileTrue(exampleSubsystem.driveElevator(() -> 0.5)); - // driver.createLeftBumper().whileTrue(exampleSubsystem.driveElevator(() -> -0.5)); - // driver.createStartButton().whileTrue(exampleSubsystem.getElevatorSysId()); + exampleCommands.configureButtonBindings(driver, operator); } @Override public void setupDefaultCommands(Controller driver, Controller operator) { - driver.setRightTrigger(driver.createRightTrigger()); - exampleSubsystem.setDefaultCommand( - exampleSubsystem.getDefaultCommand(() -> operator.getLeftYAxis())); + exampleCommands.setDefaultCommands(driver, operator); drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); } @@ -96,5 +54,6 @@ public Command generateAutoCommand(Command autoCommand) { public void buildAutoCommands() { super.buildAutoCommands(); selectableCommand.addOption("Do Nothing", Commands.none()); + drivetrain.addAutoCommands(selectableCommand); } } diff --git a/src/main/java/frc/robot/example/ExampleSubsystem.java b/src/main/java/frc/robot/example/ExampleSubsystem.java deleted file mode 100644 index 5d57eac7..00000000 --- a/src/main/java/frc/robot/example/ExampleSubsystem.java +++ /dev/null @@ -1,249 +0,0 @@ -package frc.robot.example; - -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.Meters; -import static edu.wpi.first.units.Units.MetersPerSecond; - -import com.revrobotics.spark.SparkMax; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; -import org.frc5010.common.arch.GenericSubsystem; -import org.frc5010.common.constants.GenericPID; -import org.frc5010.common.constants.MotorFeedFwdConstants; -import org.frc5010.common.drive.swerve.YAGSLSwerveDrivetrain; -import org.frc5010.common.motors.MotorConstants.Motor; -import org.frc5010.common.motors.MotorFactory; -import org.frc5010.common.motors.SystemIdentification; -import org.frc5010.common.motors.function.AngularControlMotor; -import org.frc5010.common.motors.function.PercentControlMotor; -import org.frc5010.common.motors.function.VelocityControlMotor; -import org.frc5010.common.motors.function.VerticalPositionControlMotor; -import org.frc5010.common.sensors.absolute_encoder.RevAbsoluteEncoder; -import org.frc5010.lobbinloco.FRC5010BallOnTheFly; -import org.ironmaple.simulation.IntakeSimulation; -import org.ironmaple.simulation.IntakeSimulation.IntakeSide; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.gamepieces.GamePieceProjectile; -import org.ironmaple.simulation.seasonspecific.crescendo2024.NoteOnFly; -import org.ironmaple.simulation.seasonspecific.reefscape2025.ReefscapeAlgaeOnFly; -import org.littletonrobotics.junction.Logger; -import yams.mechanisms.velocity.FlyWheel; - -public class ExampleSubsystem extends GenericSubsystem { - protected PercentControlMotor motor; - protected VelocityControlMotor controlledMotor; - protected AngularControlMotor angularMotor; - protected VerticalPositionControlMotor verticalMotor; - protected IntakeSimulation intakeSimulation; - protected NoteOnFly noteOnFly; - protected GamePieceProjectile gamePieceProjectile; - protected int scoredNotes = 0; - protected Rotation2d rotation = new Rotation2d(Degrees.of(180)); - protected FlyWheel shooter; - - public ExampleSubsystem() { - super("example.json"); - this.motor = (PercentControlMotor) devices.get("percent_motor"); - this.controlledMotor = (VelocityControlMotor) devices.get("velocity_motor"); - this.shooter = (FlyWheel) devices.get("Shooter"); - - this.angularMotor = angularControlledMotor(); - // verticalMotor = verticalControlledMotor(); - intakeSimulation = - IntakeSimulation.InTheFrameIntake( - "FRC5010Ball", - YAGSLSwerveDrivetrain.getSwerveDrive().getMapleSimDrive().get(), - Inches.of(24.25), - IntakeSide.FRONT, - 1); - } - - public AngularControlMotor angularControlledMotor() { - AngularControlMotor angularMotor = - new AngularControlMotor( - MotorFactory.Spark(13, Motor.Neo), "angular", getDisplayValuesHelper()) - .setupSimulatedMotor( - (5.0 * 68.0 / 24.0) * (80.0 / 24.0), - Units.lbsToKilograms(22), - Inches.of(19), - Degrees.of(0), - Degrees.of(360), - false, - 0, - Degrees.of(0), - false, - 0.1) - .setVisualizer(mechanismSimulation, new Pose3d(0.75, 0, 0.25, new Rotation3d())); - angularMotor.setEncoder(new RevAbsoluteEncoder((SparkMax) angularMotor.getMotor(), 360)); - angularMotor.setValues(new GenericPID(0.01, 0.000025, 0.003)); - angularMotor.setMotorFeedFwd(new MotorFeedFwdConstants(0.0, 0.01, 0.0, false)); - angularMotor.setIZone(3); - angularMotor.setOutputRange(-12, 12); - return angularMotor; - } - - public Command getDefaultCommand(DoubleSupplier speed) { - return Commands.run(() -> motor.set(speed.getAsDouble()), this); - } - - public AngularVelocity getVelocity() { - return shooter.getSpeed(); - } - - public Command setVelocity(AngularVelocity speed) { - return shooter.setSpeed(speed); - } - - public Command setDutyCycle(double dutyCycle) { - return shooter.set(dutyCycle); - } - - public Command setVelocity(Supplier speed) { - return shooter.setSpeed(speed); - } - - public Command setDutyCycle(Supplier dutyCycle) { - return shooter.set(dutyCycle); - } - - public Command setPercentControlMotorReference(DoubleSupplier reference) { - return Commands.runOnce( - () -> { - double speed = reference.getAsDouble(); - if (speed > 0.0 && !noteIsInsideIntake().getAsBoolean()) { - intakeSimulation.startIntake(); - } else { - intakeSimulation.stopIntake(); - } - motor.set(speed); - }, - this); - } - - public Command sysIdShooter() { - return SystemIdentification.getSysIdFullCommand( - SystemIdentification.rpmSysIdRoutine(shooter.getMotor(), logPrefix, this), 5, 3, 3); - } - - public Command setVelocityControlMotorReference(DoubleSupplier reference) { - return Commands.runOnce( - () -> { - double speed = reference.getAsDouble(); - if (speed <= 0.0 && !noteIsInsideIntake().getAsBoolean()) { - controlledMotor.setReference(speed); - } else if (speed > 3000 - && noteIsInsideIntake().getAsBoolean() - && obtainedGamePieceToScore().getAsBoolean()) { - controlledMotor.setReference(speed); - if (RobotBase.isSimulation()) { - Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); - gamePieceProjectile = - new ReefscapeAlgaeOnFly( - worldPose.getTranslation(), - controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), - YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), - worldPose.getRotation(), - Meters.of(0.45), - MetersPerSecond.of(speed / 6000 * 20), - Degrees.of(55)); - SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); - } - } else if (speed < 3000 - && speed > 1000 - && noteIsInsideIntake().getAsBoolean() - && obtainedGamePieceToScore().getAsBoolean()) { - controlledMotor.setReference(speed); - if (RobotBase.isSimulation()) { - Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); - gamePieceProjectile = - new ReefscapeAlgaeOnFly( - worldPose.getTranslation(), - controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), - YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), - worldPose.getRotation(), - Meters.of(0.45), - MetersPerSecond.of(speed / 6000 * 20), - Degrees.of(55)); - SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); - } - } else { - controlledMotor.setReference(speed); - } - }, - this); - } - - public Command setAngularMotorReference(DoubleSupplier reference) { - return Commands.runOnce(() -> angularMotor.setReference(reference.getAsDouble()), this); - } - - public Trigger obtainedGamePieceToScore() { - return new Trigger( - () -> { - return RobotBase.isSimulation() - ? intakeSimulation.getGamePiecesAmount() == 1 - && intakeSimulation.obtainGamePieceFromIntake() - : false; - }); - } - - public Trigger noteIsInsideIntake() { - return new Trigger( - () -> { - return RobotBase.isSimulation() ? intakeSimulation.getGamePiecesAmount() > 0 : false; - }); - } - - public Command addBallToRobot() { - return Commands.runOnce(() -> intakeSimulation.addGamePieceToIntake()); - } - - public Command launchBall() { - return Commands.runOnce( - () -> { - if (RobotBase.isSimulation()) { - Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); - gamePieceProjectile = - new FRC5010BallOnTheFly( - worldPose.getTranslation(), - controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), - YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), - worldPose.getRotation(), - Meters.of(0.45), - MetersPerSecond.of(10), - Degrees.of(55)) - .withProjectileTrajectoryDisplayCallBack( - (pose3ds) -> { - Logger.recordOutput( - logPrefix + "/GPTrajectory", pose3ds.toArray(Pose3d[]::new)); - }); - SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); - } - }); - } - - @Override - public void periodic() { - super.periodic(); - angularMotor.periodicUpdate(); - // verticalMotor.draw(); - } - - @Override - public void simulationPeriodic() { - super.simulationPeriodic(); - angularMotor.simulationUpdate(); - // verticalMotor.simulationUpdate(); - } -} diff --git a/src/main/java/frc/robot/example/commands/ExampleCommands.java b/src/main/java/frc/robot/example/commands/ExampleCommands.java new file mode 100644 index 00000000..99bdb403 --- /dev/null +++ b/src/main/java/frc/robot/example/commands/ExampleCommands.java @@ -0,0 +1,131 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.example.commands; + +import static edu.wpi.first.units.Units.Inches; + +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import frc.robot.example.subsystems.ExampleSubsystem; +import java.util.Map; +import org.frc5010.common.arch.GenericSubsystem; +import org.frc5010.common.arch.StateMachine; +import org.frc5010.common.arch.StateMachine.State; +import org.frc5010.common.config.ConfigConstants; +import org.frc5010.common.drive.GenericDrivetrain; +import org.frc5010.common.sensors.Controller; +import org.frc5010.common.telemetry.DisplayString; +import org.frc5010.common.telemetry.DisplayValuesHelper; + +/** Add your docs here. */ +public class ExampleCommands { + private StateMachine stateMachine; + private DisplayString commandState; + private DisplayValuesHelper DisplayHelper; + private State intakeState; + private State lowState; + private State prepState; + private State readyState; + private ExampleSubsystem launcher; + private GenericDrivetrain drivetrain; + private Map subsystems; + private Translation2d target = new Translation2d(Inches.of(182.11), Inches.of(158.84)); + + private static enum LauncherState { + INTAKE, + LOW_SPEED, + PREP_SHOOT, + READY_TO_SHOOT + } + + private LauncherState requestedState = LauncherState.INTAKE; + + public ExampleCommands(Map subsystems) { + this.subsystems = subsystems; + DisplayHelper = new DisplayValuesHelper("LauncherCommands", "Values"); + commandState = DisplayHelper.makeDisplayString("Launcher State"); + + launcher = (ExampleSubsystem) subsystems.get(ExampleSubsystem.class.getSimpleName()); + drivetrain = (GenericDrivetrain) this.subsystems.get(ConfigConstants.DRIVETRAIN); + + stateMachine = new StateMachine("LauncherStateMachine"); + intakeState = stateMachine.addState("INTAKE", intakeStateCommand()); + lowState = stateMachine.addState("LOW-SPEED", lowStateCommand()); + prepState = stateMachine.addState("PREP-SHOOT", prepStateCommand()); + readyState = stateMachine.addState("READY-TO-SHOOT", readyStateCommand()); + stateMachine.setInitialState(intakeState); + } + + public void setDefaultCommands(Controller driver, Controller operator) { + if (launcher != null) { + stateMachine.addRequirements(launcher); + launcher.setDefaultCommand(stateMachine); + } + } + + public void configureButtonBindings(Controller driver, Controller operator) { + driver.createRightBumper().onTrue(shouldPrepCommand()).onFalse(shouldIntakeCommand()); + driver.createLeftBumper().onTrue(shouldShootCommand()).onFalse(shouldPrepCommand()); + driver.createAButton().onTrue(shouldIntakeCommand()).onFalse(shouldUseLowSpeed()); + + driver.createBButton().whileTrue(launcher.sysIdPivot()); + + lowState.switchTo(prepState).when(() -> requestedState == LauncherState.PREP_SHOOT); + prepState.switchTo(lowState).when(() -> requestedState == LauncherState.LOW_SPEED); + + prepState.switchTo(readyState).when(() -> requestedState == LauncherState.READY_TO_SHOOT); + readyState.switchTo(prepState).when(() -> requestedState == LauncherState.PREP_SHOOT); + + intakeState.switchTo(lowState).when(() -> requestedState == LauncherState.LOW_SPEED); + lowState.switchTo(intakeState).when(() -> requestedState == LauncherState.INTAKE); + } + + public Command shouldPrepCommand() { + return Commands.runOnce(() -> requestedState = LauncherState.PREP_SHOOT); + } + + public Command shouldUseLowSpeed() { + return Commands.runOnce(() -> requestedState = LauncherState.LOW_SPEED); + } + + public Command shouldShootCommand() { + return Commands.runOnce(() -> requestedState = LauncherState.READY_TO_SHOOT); + } + + public Command shouldIntakeCommand() { + return Commands.runOnce(() -> requestedState = LauncherState.INTAKE); + } + + private Translation2d getTargetPose() { + return target.minus(drivetrain.getPoseEstimator().getCurrentPose().getTranslation()); + } + + private Command intakeStateCommand() { + return Commands.parallel( + Commands.runOnce(() -> commandState.setValue("Intake")), + launcher.stopTrackingCommand(), + launcher.intakeCommand()); + } + + private Command lowStateCommand() { + return Commands.parallel( + Commands.runOnce(() -> commandState.setValue("Low Speed")), + launcher.stopIntakeCommand(), + launcher.trackTargetCommand(() -> getTargetPose())); + } + + private Command prepStateCommand() { + return Commands.parallel( + Commands.runOnce(() -> commandState.setValue("Prep")), + launcher.trackTargetCommand(() -> getTargetPose())); + } + + private Command readyStateCommand() { + return Commands.parallel( + Commands.runOnce(() -> commandState.setValue("Ready")), + launcher.trackTargetCommand(() -> getTargetPose())); + } +} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleIO.java b/src/main/java/frc/robot/example/subsystems/ExampleIO.java new file mode 100644 index 00000000..89a53ee0 --- /dev/null +++ b/src/main/java/frc/robot/example/subsystems/ExampleIO.java @@ -0,0 +1,79 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.example.subsystems; + +import static edu.wpi.first.units.Units.Degrees; + +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import org.littletonrobotics.junction.AutoLog; + +/** Add your docs here. */ +public interface ExampleIO { + @AutoLog + public static class ExampleIOInputs { + public double shooterSpeedDesired = 0.0; + public double elevatorHeightDesired = 0.0; + public Angle hoodAngleDesired = Degrees.of(0.0); + public Angle turretAngleDesired = Degrees.of(0.0); + + public double shooterSpeedActual = 0.0; + public double elevatorHeightActual = 0.0; + public Angle hoodAngleActual = Degrees.of(0.0); + public Angle turretAngleActual = Degrees.of(0.0); + + public boolean shooterSpeedAtGoal = false; + public boolean elevatorHeightAtGoal = false; + public boolean hoodAngleAtGoal = false; + public boolean turretAngleAtGoal = false; + + public double shooterSpeedError = 0.0; + public double elevatorHeightError = 0.0; + public double hoodAngleError = 0.0; + public double turretAngleError = 0.0; + + public double hoodVelocity = 0.0; + public double turretVelocity = 0.0; + public double elevatorVelocity = 0.0; + public double shooterMotorOutput = 0.0; + public double elevatorMotorOutput = 0.0; + } + + public default void updateInputs(ExampleIOInputs inputs) {} + + public default void updateSimulation() {} + + public void setPercentMotor(double output); + + public Command setDutyCycle(double output); + + public void runShooter(double speed); + + public Command setUpperSpeed(AngularVelocity speed); + + public Command setElevatorHeight(double height); + + public void setHoodAngle(Angle angle); + + public void setTurretRotation(Angle angle); + + public AngularVelocity getShooterVelocity(); + + public Command sysIdShooter(); + + public Command sysIdArm(); + + public Command sysIdPivot(); + + public Command sysIdTurret(); + + public default Command addBallToRobot() { + return Commands.none(); + } + + public Command launchBall(); +} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleIOReal.java b/src/main/java/frc/robot/example/subsystems/ExampleIOReal.java new file mode 100644 index 00000000..6bfd0a00 --- /dev/null +++ b/src/main/java/frc/robot/example/subsystems/ExampleIOReal.java @@ -0,0 +1,117 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.example.subsystems; + +import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.units.Units.Volts; + +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import java.util.Map; +import org.frc5010.common.arch.GenericSubsystem; +import org.frc5010.common.motors.SystemIdentification; +import org.frc5010.common.motors.function.AngularControlMotor; +import org.frc5010.common.motors.function.PercentControlMotor; +import org.frc5010.common.motors.function.VelocityControlMotor; +import yams.mechanisms.positional.Arm; +import yams.mechanisms.positional.Pivot; +import yams.mechanisms.velocity.FlyWheel; + +/** Add your docs here. */ +public class ExampleIOReal implements ExampleIO { + protected Map devices; + protected PercentControlMotor percentMotor; + protected VelocityControlMotor controlledMotor; + protected AngularControlMotor angularMotor; + protected FlyWheel shooter; + protected Arm arm; + protected GenericSubsystem parent; + protected Pivot pivot; + + public ExampleIOReal(Map devices, GenericSubsystem parent) { + this.devices = devices; + this.parent = parent; + this.percentMotor = (PercentControlMotor) devices.get("percent_motor"); + this.controlledMotor = (VelocityControlMotor) devices.get("velocity_motor"); + this.shooter = (FlyWheel) devices.get("Shooter"); + this.arm = (Arm) devices.get("Hood"); + this.pivot = (Pivot) devices.get("Turret"); + this.angularMotor = (AngularControlMotor) devices.get("angular_motor"); + } + + @Override + public void updateInputs(ExampleIOInputs inputs) { + + angularMotor.periodicUpdate(); + } + + @Override + public void setPercentMotor(double output) { + percentMotor.set(output); + } + + @Override + public void runShooter(double speed) { + shooter.getMotor().setDutyCycle(speed); + } + + @Override + public Command setUpperSpeed(AngularVelocity speed) { + return shooter.setSpeed(speed); + } + + @Override + public Command setElevatorHeight(double height) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'setElevatorHeight'"); + } + + @Override + public void setHoodAngle(Angle angle) { + arm.getMotorController().setPosition(angle); + } + + @Override + public void setTurretRotation(Angle angle) {} + + public AngularVelocity getShooterVelocity() { + return shooter.getSpeed(); + } + + public Command sysIdShooter() { + return SystemIdentification.getSysIdFullCommand( + SystemIdentification.rpmSysIdRoutine(shooter.getMotor(), parent.getName(), parent), + 5, + 3, + 3); + } + + public Command sysIdTurret() { + return SystemIdentification.getSysIdFullCommand( + SystemIdentification.angleSysIdRoutine( + pivot.getMotorController(), parent.getName(), parent), + 5, + 3, + 3); + } + + public Command sysIdArm() { + return arm.sysId(Volts.of(12), Volts.of(1).per(Seconds), Seconds.of(10)); + } + + public Command sysIdPivot() { + return pivot.sysId(Volts.of(12), Volts.of(1).per(Seconds), Seconds.of(10)); + } + + public Command launchBall() { + return shooter.set(0); + } + + public Command setDutyCycle(double output) { + return Commands.runOnce(() -> percentMotor.set(output), parent); + } +} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleIOSim.java b/src/main/java/frc/robot/example/subsystems/ExampleIOSim.java new file mode 100644 index 00000000..3527c0a1 --- /dev/null +++ b/src/main/java/frc/robot/example/subsystems/ExampleIOSim.java @@ -0,0 +1,115 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot.example.subsystems; + +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.Inches; +import static edu.wpi.first.units.Units.Meters; +import static edu.wpi.first.units.Units.MetersPerSecond; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import java.util.Map; +import org.frc5010.common.arch.GenericSubsystem; +import org.frc5010.common.drive.GenericDrivetrain; +import org.frc5010.common.drive.swerve.YAGSLSwerveDrivetrain; +import org.frc5010.lobbinloco.FRC5010BallOnTheFly; +import org.littletonrobotics.junction.Logger; +import swervelib.simulation.ironmaple.simulation.IntakeSimulation; +import swervelib.simulation.ironmaple.simulation.IntakeSimulation.IntakeSide; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.gamepieces.GamePieceProjectile; +import swervelib.simulation.ironmaple.simulation.seasonspecific.crescendo2024.NoteOnFly; +import swervelib.simulation.ironmaple.simulation.seasonspecific.rebuilt2026.RebuiltFuelOnFly; + +/** Add your docs here. */ +public class ExampleIOSim extends ExampleIOReal { + protected IntakeSimulation intakeSimulation; + protected NoteOnFly noteOnFly; + protected RebuiltFuelOnFly fuelOnFly; + protected GamePieceProjectile gamePieceProjectile; + + public ExampleIOSim(Map devices, GenericSubsystem parent) { + super(devices, parent); + intakeSimulation = + IntakeSimulation.InTheFrameIntake( + "FRC5010Ball", + GenericDrivetrain.getMapleSimDrive().get(), + Inches.of(24.25), + IntakeSide.FRONT, + 1); + } + + @Override + public void updateSimulation() { + angularMotor.simulationUpdate(); + } + + @Override + public void setPercentMotor(double speed) { + if (speed > 0.0 && !noteIsInsideIntake().getAsBoolean()) { + intakeSimulation.startIntake(); + } else { + intakeSimulation.stopIntake(); + } + super.setPercentMotor(speed); + } + + public Trigger obtainedGamePieceToScore() { + return new Trigger( + () -> { + return RobotBase.isSimulation() + ? intakeSimulation.getGamePiecesAmount() == 1 + && intakeSimulation.obtainGamePieceFromIntake() + : false; + }); + } + + public Trigger noteIsInsideIntake() { + return new Trigger( + () -> { + return RobotBase.isSimulation() ? intakeSimulation.getGamePiecesAmount() > 0 : false; + }); + } + + public Command addBallToRobot() { + return Commands.runOnce(() -> intakeSimulation.addGamePieceToIntake()); + } + + @Override + public Command launchBall() { + return super.launchBall() + .alongWith( + Commands.runOnce( + () -> { + if (RobotBase.isSimulation()) { + Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); + gamePieceProjectile = + new FRC5010BallOnTheFly( + worldPose.getTranslation(), + controlledMotor + .getRobotToMotor() + .getTranslation() + .toTranslation2d(), + YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), + worldPose.getRotation(), + Meters.of(0.45), + MetersPerSecond.of(10), + Degrees.of(55)) + .withProjectileTrajectoryDisplayCallBack( + (pose3ds) -> { + Logger.recordOutput( + parent.getName() + "/GPTrajectory", + pose3ds.toArray(Pose3d[]::new)); + }); + SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); + } + })); + } +} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java b/src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java new file mode 100644 index 00000000..b8398c82 --- /dev/null +++ b/src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java @@ -0,0 +1,179 @@ +package frc.robot.example.subsystems; + +import static edu.wpi.first.units.Units.Degrees; +import static edu.wpi.first.units.Units.Inches; + +import com.revrobotics.spark.SparkMax; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import frc.robot.example.subsystems.ExampleIO.ExampleIOInputs; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; +import org.frc5010.common.constants.GenericPID; +import org.frc5010.common.constants.MotorFeedFwdConstants; +import org.frc5010.common.motors.MotorConstants.Motor; +import org.frc5010.common.motors.MotorFactory; +import org.frc5010.common.motors.function.AngularControlMotor; +import org.frc5010.common.sensors.absolute_encoder.RevAbsoluteEncoder; + +public class ExampleSubsystem extends GenericSubsystem { + protected ExampleIO io; + protected ExampleIOInputs inputs = new ExampleIOInputs(); + protected int scoredNotes = 0; + protected Rotation2d rotation = new Rotation2d(Degrees.of(180)); + + public ExampleSubsystem() { + super("example.json"); + devices.put("angular_motor", angularControlledMotor()); + if (RobotBase.isSimulation()) { + io = new ExampleIOSim(devices, this); + } else { + io = new ExampleIOReal(devices, this); + } + } + + public Command trackTargetCommand(Supplier targetSupplier) { + return Commands.run( + () -> { + Translation2d targetPose = targetSupplier.get(); + targetPose.getNorm(); + io.setTurretRotation(targetPose.getAngle().getMeasure()); + io.setHoodAngle(Degrees.of(45)); + }); + } + + public Command stopTrackingCommand() { + return Commands.runOnce( + () -> { + io.setTurretRotation(Degrees.of(0)); + io.setHoodAngle(Degrees.of(0)); + }); + } + + public Command intakeCommand() { + return Commands.runOnce(() -> io.setPercentMotor(0.5), this); + } + + public Command stopIntakeCommand() { + return Commands.runOnce(() -> io.setPercentMotor(0.0), this); + } + + public Command sysIdShooter() { + return io.sysIdShooter(); + } + + public Command addBallToRobot() { + return io.addBallToRobot(); + } + + public Command launchBall() { + return io.launchBall(); + } + + public Command sysIdArm() { + return io.sysIdArm(); + } + + public Command sysIdPivot() { + return io.sysIdPivot(); + } + + public Command sysIdTurret() { + return io.sysIdTurret(); + } + + @Override + public void periodic() { + super.periodic(); + io.updateInputs(inputs); + } + + @Override + public void simulationPeriodic() { + super.simulationPeriodic(); + io.updateSimulation(); + } + + public AngularControlMotor angularControlledMotor() { + AngularControlMotor angularMotor = + new AngularControlMotor( + MotorFactory.Spark(13, Motor.Neo), "angular", getDisplayValuesHelper()) + .setupSimulatedMotor( + (5.0 * 68.0 / 24.0) * (80.0 / 24.0), + Units.lbsToKilograms(22), + Inches.of(19), + Degrees.of(0), + Degrees.of(360), + false, + 0, + Degrees.of(0), + false, + 0.1) + .setVisualizer(mechanismSimulation, new Pose3d(0.75, 0, 0.25, new Rotation3d())); + angularMotor.setEncoder(new RevAbsoluteEncoder((SparkMax) angularMotor.getMotor(), 360)); + angularMotor.setValues(new GenericPID(0.01, 0.000025, 0.003)); + angularMotor.setMotorFeedFwd(new MotorFeedFwdConstants(0.0, 0.01, 0.0, false)); + angularMotor.setIZone(3); + angularMotor.setOutputRange(-12, 12); + return angularMotor; + } + + // public Command setVelocityControlMotorReference(DoubleSupplier reference) { + // return Commands.runOnce( + // () -> { + // double speed = reference.getAsDouble(); + // if (speed <= 0.0 && !noteIsInsideIntake().getAsBoolean()) { + // controlledMotor.setReference(speed); + // } else if (speed > 3000 + // && noteIsInsideIntake().getAsBoolean() + // && obtainedGamePieceToScore().getAsBoolean()) { + // controlledMotor.setReference(speed); + // if (RobotBase.isSimulation()) { + // Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); + // gamePieceProjectile = + // new ReefscapeAlgaeOnFly( + // worldPose.getTranslation(), + // controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), + // YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), + // worldPose.getRotation(), + // Meters.of(0.45), + // MetersPerSecond.of(speed / 6000 * 20), + // Degrees.of(55)); + // SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); + // } + // } else if (speed < 3000 + // && speed > 1000 + // && noteIsInsideIntake().getAsBoolean() + // && obtainedGamePieceToScore().getAsBoolean()) { + // controlledMotor.setReference(speed); + // if (RobotBase.isSimulation()) { + // Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); + // gamePieceProjectile = + // new ReefscapeAlgaeOnFly( + // worldPose.getTranslation(), + // controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), + // YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), + // worldPose.getRotation(), + // Meters.of(0.45), + // MetersPerSecond.of(speed / 6000 * 20), + // Degrees.of(55)); + // SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); + // } + // } else { + // controlledMotor.setReference(speed); + // } + // }, + // this); + // } + + // public Command setAngularMotorReference(DoubleSupplier reference) { + // return Commands.runOnce(() -> angularMotor.setReference(reference.getAsDouble()), this); + // } + +} diff --git a/src/main/java/frc/robot/generated/TunerConstants.java b/src/main/java/frc/robot/generated/TunerConstants.java deleted file mode 100644 index a96684d4..00000000 --- a/src/main/java/frc/robot/generated/TunerConstants.java +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright (c) 2021-2025 Littleton Robotics -// http://github.com/Mechanical-Advantage -// -// Use of this source code is governed by a BSD -// license that can be found in the LICENSE file -// at the root directory of this project. - -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.*; - -// 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 { - // 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(1.91) - .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, FusedCANcoder/SyncCANcoder automatically fall back to RemoteCANcoder - 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("rio", "./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.69); - - // 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.8181818181818183; - - private static final double kDriveGearRatio = 7.363636363636365; - private static final double kSteerGearRatio = 15.42857142857143; - private static final Distance kWheelRadius = Inches.of(2.167); - - private static final boolean kInvertLeftSide = true; - private static final boolean kInvertRightSide = true; - - public static final int kPigeonId = 50; - - // 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 = 1; - private static final Angle kFrontLeftEncoderOffset = Rotations.of(0.15234375); - private static final boolean kFrontLeftSteerMotorInverted = false; - private static final boolean kFrontLeftEncoderInverted = false; - - private static final Distance kFrontLeftXPos = Inches.of(10); - private static final Distance kFrontLeftYPos = Inches.of(10); - - // Front Right - private static final int kFrontRightDriveMotorId = 7; - private static final int kFrontRightSteerMotorId = 8; - private static final int kFrontRightEncoderId = 0; - private static final Angle kFrontRightEncoderOffset = Rotations.of(-0.4873046875); - private static final boolean kFrontRightSteerMotorInverted = false; - private static final boolean kFrontRightEncoderInverted = false; - - private static final Distance kFrontRightXPos = Inches.of(10); - private static final Distance kFrontRightYPos = Inches.of(-10); - - // Back Left - private static final int kBackLeftDriveMotorId = 3; - private static final int kBackLeftSteerMotorId = 4; - private static final int kBackLeftEncoderId = 3; - private static final Angle kBackLeftEncoderOffset = Rotations.of(-0.219482421875); - private static final boolean kBackLeftSteerMotorInverted = false; - private static final boolean kBackLeftEncoderInverted = false; - - private static final Distance kBackLeftXPos = Inches.of(-10); - private static final Distance kBackLeftYPos = Inches.of(10); - - // Back Right - private static final int kBackRightDriveMotorId = 5; - private static final int kBackRightSteerMotorId = 6; - private static final int kBackRightEncoderId = 2; - private static final Angle kBackRightEncoderOffset = Rotations.of(0.17236328125); - private static final boolean kBackRightSteerMotorInverted = false; - private static final boolean kBackRightEncoderInverted = false; - - private static final Distance kBackRightXPos = Inches.of(-10); - private static final Distance kBackRightYPos = Inches.of(-10); - - 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/org/frc5010/common/arch/GenericMechanism.java b/src/main/java/org/frc5010/common/arch/GenericMechanism.java index 73c4b6b1..b6e2772f 100644 --- a/src/main/java/org/frc5010/common/arch/GenericMechanism.java +++ b/src/main/java/org/frc5010/common/arch/GenericMechanism.java @@ -22,11 +22,8 @@ public abstract class GenericMechanism implements WpiHelperInterface { protected String logPrefix = getClass().getSimpleName(); /** The display values helper */ protected final DisplayValuesHelper DashBoard; - /** - * Constructor for GenericMechanism - * - * @param tabName - the name of the display tab - */ + + /** Constructor for GenericMechanism */ public GenericMechanism() { this.mechVisual = new LoggedMechanism2d(RobotConstantsDef.robotVisualH, RobotConstantsDef.robotVisualV); @@ -52,6 +49,14 @@ public GenericMechanism(String tabName) { */ public abstract void configureButtonBindings(Controller driver, Controller operator); + /** + * configureAltButtonBindings should map button/axis controls to commands + * + * @param driver - driver joystick + * @param operator - operator joystick + */ + public void configureAltButtonBindings(Controller driver, Controller operator) {} + /** * setupDefaultCommands should setup the default commands needed by subsystems It could check for * Test mode and enable different commands @@ -67,7 +72,7 @@ public GenericMechanism(String tabName) { * @param driver the driver controller * @param operator the operator controller */ - public void setupTestDefaultCommmands(Controller driver, Controller operator) {} + public void setupAltDefaultCommmands(Controller driver, Controller operator) {} /** * initRealOrSim should check the real or simulation state of the robot and initialize its code @@ -90,5 +95,8 @@ protected void setupPreferences() {} public abstract Command generateAutoCommand(Command autoCommand); /** Executed periodically when robot is disabled */ - public void disabledBehavior() {} + public void disabledInit() {} + + /** Executed periodically when robot is disabled */ + public void disabledPeriodic() {} } diff --git a/src/main/java/org/frc5010/common/arch/GenericRobot.java b/src/main/java/org/frc5010/common/arch/GenericRobot.java index d41a0a0e..ed86f977 100644 --- a/src/main/java/org/frc5010/common/arch/GenericRobot.java +++ b/src/main/java/org/frc5010/common/arch/GenericRobot.java @@ -135,7 +135,7 @@ protected void initializeDisplay() { .schedule( Commands.run( () -> { - allianceDisplay.setValue(determineAlliance()); + determineAlliance(); LEDStrip.changeSegmentPattern( ConfigConstants.ALL_LEDS, LEDStrip.getSolidPattern(allianceWpiColor)); }) @@ -193,20 +193,43 @@ public void configureButtonBindings() { configureButtonBindings(driver.orElse(null), operator.orElse(null)); } - /** Setup default commands depending on the robot mode */ + /** + * Use this method to define your button->command mappings. Buttons can be created by + * instantiating a {@link GenericHID} or one of its subclasses ({@link + * edu.wpi.first.wpilibj.Joystick} or {@link XboxController}), and then passing it to a {@link + * edu.wpi.first.wpilibj2.command.button.JoystickButton}. + */ + public void configureAltButtonBindings() { + configureAltButtonBindings(driver.orElse(null), operator.orElse(null)); + } + + /** + * Sets up the default commands for the robot. If the robot is in teleoperated or autonomous mode, + * it will call the setupDefaultCommands method. If the robot is in test mode, it will call the + * setupTestDefaultCommands method. + */ public void setupDefaultCommands() { if (DriverStation.isTeleop() || DriverStation.isAutonomous()) { setupDefaultCommands(driver.orElse(null), operator.orElse(null)); } else if (DriverStation.isTest()) { - setupTestDefaultCommmands(driver.orElse(null), operator.orElse(null)); + setupAltDefaultCommmands(driver.orElse(null), operator.orElse(null)); + } + } + + public void setupAltDefaultCommands() { + if (DriverStation.isTest()) { + setupAltDefaultCommmands(driver.orElse(null), operator.orElse(null)); } } - /** Build the auto commands and command chooser */ + /** + * Builds the auto commands and adds them to the auto selector + * + *

This should be called during robot initialization + */ public void buildAutoCommands() { initAutoCommands(); - // TODO: Figure out Pathplanner Warmup Command if (AutoBuilder.isConfigured()) { selectableCommand = new LoggedDashboardChooser<>("Auto Modes", AutoBuilder.buildAutoChooser()); @@ -243,7 +266,7 @@ public Command getAutonomousCommand() { /** Executes periodic behavior when the robot is disabled. */ @Override - public void disabledBehavior() { + public void disabledPeriodic() { selectableCommand.periodic(); } @@ -266,6 +289,7 @@ public Alliance determineAllianceColor() { public String determineAlliance() { Optional color = DriverStation.getAlliance(); alliance = color.orElse(Alliance.Blue); + allianceDisplay.setValue(alliance.name()); allianceColor5010 = color.map(it -> it == Alliance.Red ? Color.RED : Color.BLUE).orElse(Color.ORANGE); allianceWpiColor = diff --git a/src/main/java/org/frc5010/common/arch/StateMachine.java b/src/main/java/org/frc5010/common/arch/StateMachine.java index a4b1bf49..a68fd114 100644 --- a/src/main/java/org/frc5010/common/arch/StateMachine.java +++ b/src/main/java/org/frc5010/common/arch/StateMachine.java @@ -1,10 +1,11 @@ -// Original source: https://github.com/tom131313/AdvancedCommandBasedExamples/tree/main +// Original source: https://github.com/tom131313/AdvancedCommanddExamples/tree/main package org.frc5010.common.arch; import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; import edu.wpi.first.wpilibj.event.EventLoop; import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.ScheduleCommand; import edu.wpi.first.wpilibj2.command.WrapperCommand; @@ -208,7 +209,8 @@ public void initialize() { events.clear(); // make sure clear in case there would be a race between the execute poll and // the next command clear (maybe used if FSM can start/stop which it can't right // now) - new ScheduleCommand(initialState.stateCommandAugmented).schedule(); + CommandScheduler.getInstance() + .schedule(new ScheduleCommand(initialState.stateCommandAugmented)); } /** Called repeatedly while the StateMachine is running to check for triggering events. */ diff --git a/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java b/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java index e24eed50..ba225e34 100644 --- a/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java +++ b/src/main/java/org/frc5010/common/auto/pathplanner/PathFinderCommand.java @@ -50,7 +50,7 @@ public class PathFinderCommand extends Command { private final PathFollowingController controller; private final RobotConfig robotConfig; private final BooleanSupplier shouldFlipPath; - private boolean pregeneratedPath = false; + // private boolean pregeneratedPath = false; private static PathPlannerTrajectory trajPregen; private static PathPlannerPath pathPregen; @@ -446,7 +446,7 @@ public boolean isFinished() { @Override public void end(boolean interrupted) { CommandRunning = false; - pregeneratedPath = false; + // pregeneratedPath = false; timer.stop(); // Only output 0 speeds when ending a path that is supposed to stop, this allows interrupting diff --git a/src/main/java/org/frc5010/common/commands/AkitDriveCommands.java b/src/main/java/org/frc5010/common/commands/AkitDriveCommands.java new file mode 100644 index 00000000..15be4756 --- /dev/null +++ b/src/main/java/org/frc5010/common/commands/AkitDriveCommands.java @@ -0,0 +1,551 @@ +// Copyright 2021-2025 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +package org.frc5010.common.commands; + +import static edu.wpi.first.units.Units.Volts; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.controller.ProfiledPIDController; +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.trajectory.TrapezoidProfile; +import edu.wpi.first.math.util.Units; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.DriverStation.Alliance; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; +import org.frc5010.common.drive.swerve.GenericSwerveDrivetrain; +import org.frc5010.common.drive.swerve.akit.AkitSwerveDrive; + +/** + * Factory class for creating swerve drive related commands including manual drive control, + * characterization routines, and PID tuning commands for drive and steer motors. + */ +public class AkitDriveCommands { + private static final double DEADBAND = 0.1; + private static final double ANGLE_KP = 5.0; + private static final double ANGLE_KD = 0.4; + private static final double ANGLE_MAX_VELOCITY = 8.0; + private static final double ANGLE_MAX_ACCELERATION = 20.0; + 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 + private static final double PID_TUNING_VOLTAGE = 2.0; // Volts - voltage to apply during tuning + private static final double PID_TUNING_DELAY = 1.0; // Secs - initial delay before measurements + + private AkitDriveCommands() {} + + private static Translation2d getLinearVelocityFromJoysticks(double x, double y) { + // Apply deadband + double linearMagnitude = MathUtil.applyDeadband(Math.hypot(x, y), DEADBAND); + Rotation2d linearDirection = new Rotation2d(Math.atan2(y, x)); + + // Square magnitude for more precise control + linearMagnitude = linearMagnitude * linearMagnitude; + + // Return new linear velocity + return new Pose2d(new Translation2d(), linearDirection) + .transformBy(new Transform2d(linearMagnitude, 0.0, new Rotation2d())) + .getTranslation(); + } + + /** + * Field relative drive command using two joysticks (controlling linear and angular velocities). + * + * @param swerveDrive the swerve drivetrain subsystem to control + * @param drive the swerve drive implementation + * @param xSupplier supplier for the forward/backward joystick input + * @param ySupplier supplier for the left/right joystick input + * @param omegaSupplier supplier for the rotation joystick input + * @return a command that continuously reads joystick inputs and drives the robot + */ + public static Command joystickDrive( + GenericSwerveDrivetrain swerveDrive, + AkitSwerveDrive drive, + DoubleSupplier xSupplier, + DoubleSupplier ySupplier, + DoubleSupplier omegaSupplier) { + return Commands.run( + () -> { + // Get linear velocity + Translation2d linearVelocity = + getLinearVelocityFromJoysticks(xSupplier.getAsDouble(), ySupplier.getAsDouble()); + + // Apply rotation deadband + double omega = MathUtil.applyDeadband(omegaSupplier.getAsDouble(), DEADBAND); + + // Square rotation value for more precise control + omega = Math.copySign(omega * omega, omega); + + // 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.getRotation().plus(new Rotation2d(Math.PI)) + : drive.getRotation())); + }, + swerveDrive); + } + + /** + * Field relative drive command using joystick for linear control and PID for angular control. + * Possible use cases include snapping to an angle, aiming at a vision target, or controlling + * absolute rotation with a joystick. + * + * @param swerveDrive the swerve drivetrain subsystem to control + * @param drive the swerve drive implementation + * @param xSupplier supplier for the forward/backward joystick input + * @param ySupplier supplier for the left/right joystick input + * @param rotationSupplier supplier for the target rotation angle + * @return a command that drives the robot while maintaining a target orientation + */ + public static Command joystickDriveAtAngle( + GenericSwerveDrivetrain swerveDrive, + AkitSwerveDrive drive, + DoubleSupplier xSupplier, + DoubleSupplier ySupplier, + Supplier rotationSupplier) { + + // Create PID controller + ProfiledPIDController angleController = + new ProfiledPIDController( + ANGLE_KP, + 0.0, + ANGLE_KD, + new TrapezoidProfile.Constraints(ANGLE_MAX_VELOCITY, ANGLE_MAX_ACCELERATION)); + angleController.enableContinuousInput(-Math.PI, Math.PI); + + // Construct command + return Commands.run( + () -> { + // Get linear velocity + Translation2d linearVelocity = + getLinearVelocityFromJoysticks(xSupplier.getAsDouble(), ySupplier.getAsDouble()); + + // Calculate angular speed + double omega = + angleController.calculate( + drive.getRotation().getRadians(), rotationSupplier.get().getRadians()); + + // Convert to field relative speeds & send command + ChassisSpeeds speeds = + new ChassisSpeeds( + 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.getRotation().plus(new Rotation2d(Math.PI)) + : drive.getRotation())); + }, + swerveDrive) + + // Reset PID controller when command starts + .beforeStarting(() -> angleController.reset(drive.getRotation().getRadians())); + } + + /** + * Measures the velocity feedforward constants for the drive motors. + * + *

This command should only be used in voltage control mode. + * + * @param subsystem the swerve drivetrain subsystem to characterize + * @param drive the swerve drive implementation + * @param characterizer consumer that accepts voltage values to apply to drive motors + * @param velocitySupplier supplier that returns the current velocity for measurement + * @return a command that performs feedforward characterization and logs results + */ + public static Command feedforwardCharacterization( + GenericSubsystem subsystem, + Consumer characterizer, + Supplier velocitySupplier) { + List velocitySamples = new LinkedList<>(); + List voltageSamples = new LinkedList<>(); + Timer timer = new Timer(); + + return Commands.sequence( + // Reset data + Commands.runOnce( + () -> { + velocitySamples.clear(); + voltageSamples.clear(); + }), + + // Allow modules to orient + Commands.run( + () -> { + characterizer.accept(Volts.of(0.0)); + }, + subsystem) + .withTimeout(FF_START_DELAY), + + // Start timer + Commands.runOnce(timer::restart), + + // Accelerate and gather data + Commands.run( + () -> { + double voltage = timer.get() * FF_RAMP_RATE; + characterizer.accept(Volts.of(voltage)); + velocitySamples.add(velocitySupplier.get()); + voltageSamples.add(voltage); + }, + subsystem) + + // 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)); + SmartDashboard.putNumber("Characterization/Feedforward/kS", kS); + SmartDashboard.putNumber("Characterization/Feedforward/kV", kV); + })); + } + + /** + * Measures the robot's wheel radius by spinning in a circle. + * + * @param swerveDrive the swerve drivetrain subsystem to characterize + * @param drive the swerve drive implementation + * @return a command that measures wheel radius and logs the results to SmartDashboard + */ + public static Command wheelRadiusCharacterization( + GenericSwerveDrivetrain swerveDrive, AkitSwerveDrive drive) { + SlewRateLimiter limiter = new SlewRateLimiter(WHEEL_RADIUS_RAMP_RATE); + 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(WHEEL_RADIUS_MAX_VELOCITY); + drive.runVelocity(new ChassisSpeeds(0.0, 0.0, speed)); + }, + swerveDrive)), + + // Measurement sequence + Commands.sequence( + // Wait for modules to fully orient before starting measurement + Commands.waitSeconds(1.0), + + // Record starting measurement + Commands.runOnce( + () -> { + state.positions = drive.getWheelRadiusCharacterizationPositions(); + state.lastAngle = drive.getRotation(); + state.gyroDelta = 0.0; + }), + + // Update gyro delta + Commands.run( + () -> { + var rotation = drive.getRotation(); + 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; + } + double wheelRadius = + (state.gyroDelta * drive.getConfig().DRIVE_BASE_RADIUS) / 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"); + SmartDashboard.putNumber("Characterization/Wheel Radius", wheelRadius); + }))); + } + + private static class WheelRadiusCharacterizationState { + double[] positions = new double[4]; + Rotation2d lastAngle = new Rotation2d(); + double gyroDelta = 0.0; + } + + /** + * Measures the proportional gain (P value) of the drive motor PID controllers. + * + *

This command applies a range of target velocity setpoints to the drive motors and measures + * the resulting velocity errors to calculate a more robust P value for closed-loop control. + * + *

The P value is calculated using: kP = Applied Voltage / Average Velocity Error across + * multiple setpoints + * + * @param swerveDrive the swerve drivetrain subsystem to tune + * @param drive the swerve drive implementation + * @return a command that measures velocity errors across multiple setpoints and logs suggested kP + * values + */ + public static Command drivePIDTuning(GenericSwerveDrivetrain swerveDrive, AkitSwerveDrive drive) { + final double[] TARGET_VELOCITIES = {0.5, 1.0, 1.5, 2.0, 2.5, 3.0}; // Range of velocities in m/s + List allVelocityErrors = new LinkedList<>(); + + return Commands.sequence( + // Reset data + Commands.runOnce(allVelocityErrors::clear), + + // Allow modules to orient once + Commands.run(() -> drive.runCharacterization(0.0), swerveDrive) + .withTimeout(PID_TUNING_DELAY), + + // Test each target velocity + Commands.sequence( + // Create a sequence of commands, one for each target velocity + java.util.Arrays.stream(TARGET_VELOCITIES) + .boxed() + .map( + targetVelocity -> + Commands.sequence( + // Transition to new setpoint + Commands.runOnce( + () -> { + ChassisSpeeds speeds = + new ChassisSpeeds(targetVelocity, 0.0, 0.0); + drive.runVelocity(speeds); + }), + + // Wait for module to settle + Commands.waitSeconds(0.5), + + // Gather velocity error data + Commands.run( + () -> { + ChassisSpeeds speeds = + new ChassisSpeeds(targetVelocity, 0.0, 0.0); + drive.runVelocity(speeds); + + // Calculate velocity error: difference between setpoint + // and actual velocity + double avgVelocity = 0.0; + for (int i = 0; i < 4; i++) { + avgVelocity += + drive.getModulesInfo()[i] + .driveVelocityMetersPerSecond(); + } + avgVelocity /= 4.0; + double error = Math.abs(targetVelocity - avgVelocity); + allVelocityErrors.add(error); + }, + swerveDrive) + .withTimeout(1.5))) + .toArray(Command[]::new)), + + // When finished, calculate and print results + Commands.runOnce( + () -> { + if (allVelocityErrors.isEmpty()) { + System.out.println("No velocity error data collected."); + return; + } + + // Calculate average velocity error across all test points + double avgError = 0.0; + for (double error : allVelocityErrors) { + avgError += error; + } + avgError /= allVelocityErrors.size(); + + // Calculate P value: P = Voltage / Error + // This represents the proportional gain needed to achieve the targets with + // applied voltage + double kP = avgError > 0.001 ? PID_TUNING_VOLTAGE / avgError : 0.0; + + NumberFormat formatter = new DecimalFormat("#0.00000"); + System.out.println("********** Drive PID P Value Tuning Results **********"); + System.out.println( + "\tTarget Velocities: " + java.util.Arrays.toString(TARGET_VELOCITIES) + " m/s"); + System.out.println("\tNumber of Samples: " + allVelocityErrors.size()); + System.out.println( + "\tAverage Velocity Error: " + formatter.format(avgError) + " m/s"); + System.out.println("\tSuggested kP: " + formatter.format(kP)); + System.out.println("*******************************************************"); + SmartDashboard.putNumber("Characterization/Drive/P_Value", kP); + SmartDashboard.putNumber("Characterization/Drive/Avg_Velocity_Error", avgError); + SmartDashboard.putNumber( + "Characterization/Drive/Sample_Count", allVelocityErrors.size()); + })); + } + + /** + * Measures the proportional gain (P value) of the steer motor PID controllers. + * + *

This command applies a range of target angle setpoints to the steer motors and measures the + * resulting angle errors to calculate a more robust P value for closed-loop control. + * + *

The P value is calculated using: kP = Applied Voltage / Average Angle Error across multiple + * setpoints + * + * @param swerveDrive the swerve drivetrain subsystem to tune + * @param drive the swerve drive implementation + * @return a command that measures angle errors across multiple setpoints and logs suggested kP + * values + */ + public static Command steerPIDTuning(GenericSwerveDrivetrain swerveDrive, AkitSwerveDrive drive) { + final double[] TARGET_ANGLES = { + 15.0, 30.0, 45.0, 60.0, 75.0, 90.0 + }; // Range of angles in degrees + List allAngleErrors = new LinkedList<>(); + + return Commands.sequence( + // Reset data + Commands.runOnce(allAngleErrors::clear), + + // Test each target angle + Commands.sequence( + // Create a sequence of commands, one for each target angle + java.util.Arrays.stream(TARGET_ANGLES) + .boxed() + .map( + targetDegrees -> + Commands.sequence( + // Transition to new setpoint + Commands.runOnce( + () -> { + // Command all modules to the target angle + ChassisSpeeds speeds = + new ChassisSpeeds(0, 0, Math.toRadians(targetDegrees)); + + drive.runVelocity(speeds); + }), + + // Wait for modules to reach target angle + Commands.waitSeconds(1.0), + + // Measure angle error after settling + Commands.runOnce( + () -> { + double avgAngleError = 0.0; + for (int i = 0; i < 4; i++) { + double currentAngle = + drive.getModulesInfo()[i].steerAbsoluteDegrees(); + double error = Math.abs(targetDegrees - currentAngle); + // Handle angle wrapping (shortest path) + if (error > 180.0) { + error = 360.0 - error; + } + avgAngleError += error; + } + avgAngleError /= 4.0; + avgAngleError = Math.toRadians(avgAngleError); + allAngleErrors.add(avgAngleError); + }))) + .toArray(Command[]::new)), + + // When finished, calculate and print results + Commands.runOnce( + () -> { + if (allAngleErrors.isEmpty()) { + System.out.println("No angle error data collected."); + return; + } + + // Calculate average angle error across all test points + double avgError = 0.0; + for (double error : allAngleErrors) { + avgError += error; + } + avgError /= allAngleErrors.size(); + + // Calculate P value: P = Voltage / Error + double kP = avgError > 0.001 ? PID_TUNING_VOLTAGE / avgError : 0.0; + + NumberFormat formatter = new DecimalFormat("#0.00000"); + System.out.println("********** Steer PID P Value Tuning Results **********"); + System.out.println( + "\tTarget Angles: " + java.util.Arrays.toString(TARGET_ANGLES) + " degrees"); + System.out.println("\tNumber of Samples: " + allAngleErrors.size()); + System.out.println( + "\tAverage Angle Error: " + + formatter.format(Math.toDegrees(avgError)) + + " degrees"); + System.out.println("\tSuggested kP: " + formatter.format(kP)); + System.out.println("*******************************************************"); + SmartDashboard.putNumber("Characterization/Steer/P_Value", kP); + SmartDashboard.putNumber( + "Characterization/Steer/Avg_Angle_Error", Math.toDegrees(avgError)); + SmartDashboard.putNumber( + "Characterization/Steer/Sample_Count", allAngleErrors.size()); + })); + } +} diff --git a/src/main/java/org/frc5010/common/commands/DefaultDriveCommand.java b/src/main/java/org/frc5010/common/commands/DefaultDriveCommand.java index 985e6643..90c554a9 100644 --- a/src/main/java/org/frc5010/common/commands/DefaultDriveCommand.java +++ b/src/main/java/org/frc5010/common/commands/DefaultDriveCommand.java @@ -15,7 +15,6 @@ /** A default driving command */ public class DefaultDriveCommand extends Command { - // TODO: Understand code private final GenericDrivetrain drivetrainSubsystem; private final DoubleSupplier m_translationXSupplier; diff --git a/src/main/java/org/frc5010/common/commands/DriveByAngle.java b/src/main/java/org/frc5010/common/commands/DriveByAngle.java index 389c0707..8566306c 100644 --- a/src/main/java/org/frc5010/common/commands/DriveByAngle.java +++ b/src/main/java/org/frc5010/common/commands/DriveByAngle.java @@ -14,7 +14,6 @@ /** Drive the robot where the angle is given */ public class DriveByAngle extends Command { - // TODO: Understand code /** The subsystem used by this command. */ private final GenericDrivetrain drivetrainSubsystem; diff --git a/src/main/java/org/frc5010/common/commands/DriveToPoseSupplier.java b/src/main/java/org/frc5010/common/commands/DriveToPoseSupplier.java index 24b552c7..7971c87d 100644 --- a/src/main/java/org/frc5010/common/commands/DriveToPoseSupplier.java +++ b/src/main/java/org/frc5010/common/commands/DriveToPoseSupplier.java @@ -77,8 +77,8 @@ public class DriveToPoseSupplier extends GenericCommand { private Translation2d lastSetpointTranslation = Translation2d.kZero; private Rotation2d lastSetpointRotation = Rotation2d.kZero; private double lastTime = 0.0; - private double driveErrorAbs = 0.0; - private double thetaErrorAbs = 0.0; + // private double driveErrorAbs = 0.0; + // private double thetaErrorAbs = 0.0; private double speedTowardsTarget = 0.0; private Translation2d currentVelocity = new Translation2d(); private double previousTime = 0.0, deltaTime = 0.0; diff --git a/src/main/java/org/frc5010/common/commands/LedDefaultCommand.java b/src/main/java/org/frc5010/common/commands/LedDefaultCommand.java index c2726b4a..05636b5e 100644 --- a/src/main/java/org/frc5010/common/commands/LedDefaultCommand.java +++ b/src/main/java/org/frc5010/common/commands/LedDefaultCommand.java @@ -11,7 +11,7 @@ public class LedDefaultCommand extends Command { /** Creates a new LedDefaultCommand. */ LedSubsystem ledSubsystem; - private int currDelay = 200; + // private int currDelay = 200; private boolean lastState; public LedDefaultCommand(LedSubsystem ledSubsystem) { diff --git a/src/main/java/org/frc5010/common/config/ConfigConstants.java b/src/main/java/org/frc5010/common/config/ConfigConstants.java index cc622b0b..b38a093c 100644 --- a/src/main/java/org/frc5010/common/config/ConfigConstants.java +++ b/src/main/java/org/frc5010/common/config/ConfigConstants.java @@ -6,4 +6,11 @@ public class ConfigConstants { public static final String DRIVETRAIN = "drivetrain"; public static final String ALL_LEDS = "all_leds"; + + public static enum ControlAlgorithm { + SIMPLE, + PROFILED, + EXPO, + EXPO_ELEVATOR + } } diff --git a/src/main/java/org/frc5010/common/config/DeviceConfiguration.java b/src/main/java/org/frc5010/common/config/DeviceConfiguration.java index 960dcf97..6020b961 100644 --- a/src/main/java/org/frc5010/common/config/DeviceConfiguration.java +++ b/src/main/java/org/frc5010/common/config/DeviceConfiguration.java @@ -3,11 +3,49 @@ import edu.wpi.first.wpilibj2.command.SubsystemBase; import org.frc5010.common.arch.GenericSubsystem; +/** + * Interface for configuring robot devices from JSON configuration data. + * + *

Implementations of this interface define how to instantiate and configure specific robot + * hardware devices (motors, sensors, subsystems, etc.) from JSON configuration objects. This + * pattern allows robot configuration to be externalized to JSON files while maintaining type safety + * and compile-time checking. + * + *

Typical usage: + * + *

+ *   DeviceConfiguration config = objectMapper.readValue(jsonFile, ConcreteConfigClass.class);
+ *   MyDevice device = (MyDevice) config.configure(subsystem);
+ * 
+ * + * @see GenericSubsystem + * @see SubsystemBase + */ public interface DeviceConfiguration { + /** + * Configures and returns a device instance based on this configuration object. + * + *

This method is called with the subsystem that will contain the device. The implementing + * class is responsible for creating the appropriate device instance using the configuration data + * and adding it to the subsystem if necessary. + * + * @param deviceHandler the {@link SubsystemBase} to configure with this device configuration + * @return the configured device object, or {@code null} if no device was created + */ public default Object configure(SubsystemBase deviceHandler) { return null; } + /** + * Configures and returns a device instance using a {@link GenericSubsystem}. + * + *

This method delegates to {@link #configure(SubsystemBase)} after casting the {@code + * GenericSubsystem} to a {@code SubsystemBase}. Subclasses can override this method for more + * specific handling when working with {@code GenericSubsystem} instances. + * + * @param deviceHandler the {@link GenericSubsystem} to configure with this device configuration + * @return the configured device object, or {@code null} if no device was created + */ public default Object configure(GenericSubsystem deviceHandler) { return configure((SubsystemBase) deviceHandler); } diff --git a/src/main/java/org/frc5010/common/config/RobotParser.java b/src/main/java/org/frc5010/common/config/RobotParser.java index fc701cfb..6d478bf3 100644 --- a/src/main/java/org/frc5010/common/config/RobotParser.java +++ b/src/main/java/org/frc5010/common/config/RobotParser.java @@ -20,7 +20,34 @@ import org.frc5010.common.config.json.YAGSLDrivetrainJson; import org.frc5010.common.config.json.devices.LEDStripParser; -/** RobotParser is used to parse JSON configuration files to build a robot. */ +/** + * Parses JSON configuration files to initialize and build a robot's subsystems. + * + *

This class is responsible for reading and processing JSON configuration files from the robot's + * deploy directory. It orchestrates the loading and configuration of: + * + *

    + *
  • Robot base configuration (basic properties) + *
  • Driveteam controller bindings + *
  • Vision system cameras + *
  • Drivetrain (swerve or other type) + *
  • LED strips + *
+ * + *

The typical usage pattern is: + * + *

+ *   RobotParser parser = new RobotParser("myRobotDir", robot);
+ *   parser.createRobot(robot);
+ * 
+ * + *

Configuration files are expected to be located in the robot's deploy directory and must follow + * the standard FRC5010 JSON schema. + * + * @see GenericRobot + * @see DriveteamControllerConfiguration + * @see CameraConfigurationJson + */ public class RobotParser { /** JSON classes for the Driveteam controllers */ private static DriveteamControllersJson controllersJson; @@ -34,11 +61,28 @@ public class RobotParser { private static Optional driveTrainJson = Optional.empty(); /** - * Creates a new RobotParser. + * Creates a new RobotParser and initializes robot configuration from JSON files. * - * @param robotDirectory the directory to read from - * @param robot the robot being configured - * @throws IOException + *

This constructor performs the following steps: + * + *

    + *
  1. Locates the robot configuration directory + *
  2. Loads and parses robot.json for base robot configuration + *
  3. Loads and parses controllers.json for driveteam controller configuration + *
  4. Loads and parses cameras.json for vision system configuration + *
  5. Parses LED strip configurations + *
  6. Loads drivetrain configuration (YAGSL swerve or AdvantageKit swerve) + *
+ * + *

The configuration is not fully applied to the robot until {@link #createRobot(GenericRobot)} + * is called. + * + * @param robotDirectory the name of the directory (relative to the deploy directory) containing + * the robot configuration JSON files + * @param robot the {@link GenericRobot} instance that will be configured with the loaded + * configuration + * @throws IOException if any configuration file cannot be read or parsed + * @throws AssertionError if required configuration files (robot.json) are missing */ public RobotParser(String robotDirectory, GenericRobot robot) throws IOException { File directory = new File(Filesystem.getDeployDirectory(), robotDirectory); @@ -93,18 +137,36 @@ public RobotParser(String robotDirectory, GenericRobot robot) throws IOException } /** - * Method to check the existence of specific JSON configuration files in the provided directory. + * Verifies that required JSON configuration files exist in the directory. + * + *

This method ensures that critical configuration files (at minimum, robot.json) are present + * in the configuration directory before proceeding with parsing. * * @param directory the directory to check for JSON configuration files + * @throws AssertionError if required configuration files do not exist */ private void checkDirectory(File directory) { assert new File(directory, "robot.json").exists(); } /** - * Method to create the robot. + * Applies all loaded configurations to create and initialize the robot's subsystems. + * + *

This method instantiates and configures all robot subsystems based on the JSON configuration + * files that were loaded during construction. It performs: + * + *

    + *
  • Creation of driveteam controller bindings + *
  • Creation of the drivetrain subsystem (if configured) + *
  • Creation of vision/camera subsystems + *
+ * + *

This method should be called after the robot instance is fully initialized and ready to + * receive subsystems. * - * @param robot description of parameter + * @param robot the {@link GenericRobot} instance to apply configurations to + * @throws NullPointerException if required configuration data was not loaded successfully + * @see #RobotParser(String, GenericRobot) for the loading phase */ public void createRobot(GenericRobot robot) { controllersJson.createControllers(robot, controllersMap); diff --git a/src/main/java/org/frc5010/common/config/SubsystemParser.java b/src/main/java/org/frc5010/common/config/SubsystemParser.java index b6517561..78aa1a47 100644 --- a/src/main/java/org/frc5010/common/config/SubsystemParser.java +++ b/src/main/java/org/frc5010/common/config/SubsystemParser.java @@ -66,5 +66,6 @@ public void parseSubsystem(GenericSubsystem genericSubsystem, String configFile) genericSubsystem.setDisplay(subsystemJson.display); genericSubsystem.setLoggingLevel(LogLevel.valueOf(subsystemJson.logLevel)); subsystemJson.configureSubsystem(genericSubsystem, directory); + robot.addSubsystem(genericSubsystem.getClass().getSimpleName(), genericSubsystem); } } diff --git a/src/main/java/org/frc5010/common/config/UnitsParser.java b/src/main/java/org/frc5010/common/config/UnitsParser.java index 5bc5c94e..beb98826 100644 --- a/src/main/java/org/frc5010/common/config/UnitsParser.java +++ b/src/main/java/org/frc5010/common/config/UnitsParser.java @@ -14,6 +14,7 @@ import static edu.wpi.first.units.Units.FeetPerSecondPerSecond; import static edu.wpi.first.units.Units.Inches; import static edu.wpi.first.units.Units.InchesPerSecond; +import static edu.wpi.first.units.Units.KilogramSquareMeters; import static edu.wpi.first.units.Units.Kilograms; import static edu.wpi.first.units.Units.Meters; import static edu.wpi.first.units.Units.MetersPerSecond; @@ -25,6 +26,7 @@ import static edu.wpi.first.units.Units.Radians; import static edu.wpi.first.units.Units.RadiansPerSecond; import static edu.wpi.first.units.Units.RadiansPerSecondPerSecond; +import static edu.wpi.first.units.Units.Rotations; import static edu.wpi.first.units.Units.RotationsPerSecond; import static edu.wpi.first.units.Units.RotationsPerSecondPerSecond; import static edu.wpi.first.units.Units.Second; @@ -40,48 +42,66 @@ import edu.wpi.first.units.measure.LinearAcceleration; import edu.wpi.first.units.measure.LinearVelocity; import edu.wpi.first.units.measure.Mass; +import edu.wpi.first.units.measure.MomentOfInertia; import edu.wpi.first.units.measure.Time; import edu.wpi.first.units.measure.Voltage; import org.frc5010.common.config.json.UnitValueJson; +import org.frc5010.common.config.units.AngleUnit; +import org.frc5010.common.config.units.AngularAccelerationUnit; +import org.frc5010.common.config.units.AngularVelocityUnit; +import org.frc5010.common.config.units.CurrentUnit; +import org.frc5010.common.config.units.DistanceUnit; +import org.frc5010.common.config.units.LinearAccelerationUnit; +import org.frc5010.common.config.units.LinearVelocityUnit; +import org.frc5010.common.config.units.MassUnit; +import org.frc5010.common.config.units.MomentOfInertiaUnit; +import org.frc5010.common.config.units.TimeUnit; +import org.frc5010.common.config.units.VoltageUnit; /** A class that converts a magnitude and a unit into a {@link Measurement} object. */ public class UnitsParser { - public static final String M = "m"; - public static final String CM = "cm"; - public static final String MM = "mm"; - public static final String IN = "in"; - public static final String FT = "ft"; - public static final String YD = "yd"; - public static final String MPS = "m/s"; - public static final String CMPS = "cm/s"; - public static final String MPS2 = "m/s^2"; - public static final String CMPS2 = "cm/s^2"; - public static final String DEG = "deg"; - public static final String RAD = "rad"; - public static final String DEGPS = "deg/s"; - public static final String RADPS = "rad/s"; - public static final String rpm = "rpm"; - public static final String rps = "rps"; - public static final String DEGPS2 = "deg/s^2"; - public static final String RADPS2 = "rad/s^2"; - public static final String RPMPS = "rpmps"; - public static final String RPS2 = "rps^2"; - public static final String AMPS = "amps"; - public static final String VOLTS = "volts"; - public static final String SEC = "sec"; - public static final String MS = "ms"; - public static final String US = "us"; - public static final String NS = "ns"; - public static final String KG = "kg"; - public static final String G = "g"; - public static final String MG = "mg"; - public static final String OZ = "oz"; - public static final String LBS = "lbs"; - public static final String STONE = "stone"; - public static final String TONS = "tons"; - private static final String FPS = "ft/s"; - private static final String FPS2 = "ft/s^2"; + /** + * Deprecated: Use the specific unit enum classes instead. These constants are provided for + * backwards compatibility with existing code. + * + * @deprecated Use {@link DistanceUnit#METERS}, {@link LinearVelocityUnit#METERS_PER_SECOND}, etc. + */ + @Deprecated public static final String CM = "cm"; + + @Deprecated public static final String MM = "mm"; + @Deprecated public static final String IN = "in"; + @Deprecated public static final String FT = "ft"; + @Deprecated public static final String YD = "yd"; + @Deprecated public static final String MPS = "m/s"; + @Deprecated public static final String CMPS = "cm/s"; + @Deprecated public static final String MPS2 = "m/s^2"; + @Deprecated public static final String CMPS2 = "cm/s^2"; + @Deprecated public static final String DEG = "deg"; + @Deprecated public static final String RAD = "rad"; + @Deprecated public static final String DEGPS = "deg/s"; + @Deprecated public static final String RADPS = "rad/s"; + @Deprecated public static final String rpm = "rpm"; + @Deprecated public static final String rps = "rps"; + @Deprecated public static final String DEGPS2 = "deg/s^2"; + @Deprecated public static final String RADPS2 = "rad/s^2"; + @Deprecated public static final String RPMPS = "rpmps"; + @Deprecated public static final String RPS2 = "rps^2"; + @Deprecated public static final String AMPS = "amps"; + @Deprecated public static final String VOLTS = "volts"; + @Deprecated public static final String SEC = "sec"; + @Deprecated public static final String MS = "ms"; + @Deprecated public static final String US = "us"; + @Deprecated public static final String NS = "ns"; + @Deprecated public static final String KG = "kg"; + @Deprecated public static final String G = "g"; + @Deprecated public static final String MG = "mg"; + @Deprecated public static final String OZ = "oz"; + @Deprecated public static final String LBS = "lbs"; + @Deprecated public static final String STONE = "stone"; + @Deprecated public static final String TONS = "tons"; + @Deprecated private static final String FPS = "ft/s"; + @Deprecated private static final String FPS2 = "ft/s^2"; /** * Converts a magnitude and a unit into a {@link Distance} object. * @@ -101,35 +121,23 @@ public static Distance parseDistance(UnitValueJson unitValueJson) { * @return The {@link Distance} object. */ public static Distance parseDistance(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case M: - case "meter": - case "meters": + DistanceUnit distanceUnit = DistanceUnit.fromString(unit); + switch (distanceUnit) { + case METERS: return Meters.of(magnitude); - case IN: - case "inch": - case "inches": + case INCHES: return Inches.of(magnitude); - case FT: - case "foot": - case "feet": + case FEET: return Feet.of(magnitude); - case "mm": - case "millimeter": - case "millimeters": + case MILLIMETERS: return Millimeters.of(magnitude); - case CM: - case "centimeter": - case "centimeters": + case CENTIMETERS: return Centimeters.of(magnitude); - case YD: - case "yard": - case "yards": + case YARDS: return Feet.of(magnitude * 3); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to bananas"); - return Meters.of(magnitude * (0.254 - Math.random() * 0.05)); + System.err.println("Unexpected distance unit: " + distanceUnit); + return Meters.of(magnitude); } } @@ -152,45 +160,20 @@ public static LinearVelocity parseVelocity(UnitValueJson unitValueJson) { * @return The {@link LinearVelocity} object. */ public static LinearVelocity parseVelocity(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case MPS: - case "m/sec": - case "meter/sec": - case "meters/sec": - case "meter/second": - case "meters/second": + LinearVelocityUnit velocityUnit = LinearVelocityUnit.fromString(unit); + switch (velocityUnit) { + case METERS_PER_SECOND: return MetersPerSecond.of(magnitude); - case "in/s": - case "in/sec": - case "inch/sec": - case "inches/sec": - case "inch/second": - case "inches/second": + case INCHES_PER_SECOND: return InchesPerSecond.of(magnitude); - case FPS: - case "ft/sec": - case "foot/sec": - case "feet/sec": - case "foot/second": - case "feet/second": + case FEET_PER_SECOND: return FeetPerSecond.of(magnitude); - case "mm/s": - case "mm/sec": - case "millimeter/sec": - case "millimeters/sec": - case "millimeter/second": - case "millimeters/second": - return MetersPerSecond.of(magnitude * 1000); - case "cm/s": - case "cm/sec": - case "centimeter/sec": - case "centimeters/sec": - case "centimeter/second": - case "centimeters/second": - return MetersPerSecond.of(magnitude * 100); + case MILLIMETERS_PER_SECOND: + return MetersPerSecond.of(magnitude / 1000); + case CENTIMETERS_PER_SECOND: + return MetersPerSecond.of(magnitude / 100); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to meters/second."); + System.err.println("Unexpected linear velocity unit: " + velocityUnit); break; } return MetersPerSecond.of(magnitude); @@ -227,78 +210,20 @@ public static LinearAcceleration parseAccelleration(UnitValueJson unitValueJson) * @return The {@link LinearAcceleration} object. */ public static LinearAcceleration parseAccelleration(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case MPS2: - case "m/s/s": - case "m/s2": - case "m/sec/sec": - case "m/sec2": - case "m/sec^2": - case "meters/sec/sec": - case "meters/sec2": - case "meters/sec^2": - case "meters/second/second": - case "meters/second2": - case "meters/second^2": + LinearAccelerationUnit accelUnit = LinearAccelerationUnit.fromString(unit); + switch (accelUnit) { + case METERS_PER_SECOND_SQUARED: return MetersPerSecondPerSecond.of(magnitude); - case "in/s^2": - case "in/s/s": - case "in/s2": - case "in/sec/sec": - case "in/sec2": - case "in/sec^2": - case "inches/sec/sec": - case "inches/sec2": - case "inches/sec^2": - case "inches/second/second": - case "inches/second2": - case "inches/second^2": + case INCHES_PER_SECOND_SQUARED: return InchesPerSecond.of(magnitude).per(Second); - case FPS2: - case "ft/s/s": - case "ft/s2": - case "ft/sec/sec": - case "ft/sec2": - case "ft/sec^2": - case "feet/sec/sec": - case "feet/sec2": - case "feet/sec^2": - case "feet/second/second": - case "feet/second2": - case "feet/second^2": + case FEET_PER_SECOND_SQUARED: return FeetPerSecondPerSecond.of(magnitude); - case "mm/s^2": - case "mm/s/s": - case "mm/s2": - case "millimeter/sec/sec": - case "millimeter/sec2": - case "millimeter/sec^2": - case "millimeters/sec/sec": - case "millimeters/sec2": - case "millimeters/sec^2": - case "millimeters/second/second": - case "millimeters/second2": - case "millimeters/second^2": - return MetersPerSecondPerSecond.of(magnitude * 1000); - case "cm/s^2": - case "cm/sec^2": - case "cm/second^2": - case "centimeter/sec^2": - case "centimeter/second^2": - case "centimeters/sec^2": - case "centimeters/second^2": - case "cms/s^2": - case "cms/sec^2": - case "cms/second^2": - case "cm/s/s": - case "cm/sec/sec": - case "cm/second/second": - case "centimeters/sec/sec": - case "centimeters/second/second": - return MetersPerSecondPerSecond.of(magnitude * 100); + case MILLIMETERS_PER_SECOND_SQUARED: + return MetersPerSecondPerSecond.of(magnitude / 1000); + case CENTIMETERS_PER_SECOND_SQUARED: + return MetersPerSecondPerSecond.of(magnitude / 100); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to meters/second."); + System.err.println("Unexpected linear acceleration unit: " + accelUnit); break; } return MetersPerSecondPerSecond.of(magnitude); @@ -324,27 +249,16 @@ public static Current parseAmps(UnitValueJson unitValueJson) { * @return The {@link Current} object. */ public static Current parseAmps(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { + CurrentUnit currentUnit = CurrentUnit.fromString(unit); + switch (currentUnit) { case AMPS: - case "a": - case "amp": - case "ampere": - case "amperes": return Amps.of(magnitude); - case "ma": - case "milliamp": - case "milliamps": - case "milliampere": - case "milliamperes": + case MILLIAMPS: return Amps.of(magnitude * 0.001); - case "ua": - case "microamp": - case "microamps": - case "microampere": - case "microamperes": + case MICROAMPS: return Amps.of(magnitude * 0.000001); default: - System.err.println("Unknown unit: " + unit + " for " + magnitude + ". Defaulting to amps."); + System.err.println("Unexpected current unit: " + currentUnit); break; } return Amps.of(magnitude); @@ -370,27 +284,18 @@ public static Voltage parseVolts(UnitValueJson unitValueJson) { * @return The {@link Voltage} object. */ public static Voltage parseVolts(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { + VoltageUnit voltageUnit = VoltageUnit.fromString(unit); + switch (voltageUnit) { case VOLTS: - case "v": - case "volt": - case "voltage": return Volts.of(magnitude); - case "mv": - case "millivolt": - case "millivolts": + case MILLIVOLTS: return Volts.of(magnitude * 0.001); - case "uv": - case "microvolt": - case "microvolts": + case MICROVOLTS: return Volts.of(magnitude * 0.000001); - case "kv": - case "kilovolt": - case "kilovolts": + case KILOVOLTS: return Volts.of(magnitude * 1000); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to volts."); + System.err.println("Unexpected voltage unit: " + voltageUnit); break; } return Volts.of(magnitude); @@ -416,39 +321,24 @@ public static Time parseTime(UnitValueJson unitValueJson) { * @return The {@link Time} object. */ public static Time parseTime(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case "s": - case SEC: - case "second": - case "seconds": + TimeUnit timeUnit = TimeUnit.fromString(unit); + switch (timeUnit) { + case SECONDS: return Seconds.of(magnitude); - case MS: - case "millisecond": - case "milliseconds": + case MILLISECONDS: return Seconds.of(magnitude * 0.001); - case "us": - case "microsecond": - case "microseconds": + case MICROSECONDS: return Seconds.of(magnitude * 0.000001); - case NS: - case "nanosecond": - case "nanoseconds": + case NANOSECONDS: return Seconds.of(magnitude * 0.000000001); - case "min": - case "minute": - case "minutes": + case MINUTES: return Seconds.of(magnitude * 60); - case "h": - case "hour": - case "hours": + case HOURS: return Seconds.of(magnitude * 3600); - case "d": - case "day": - case "days": + case DAYS: return Seconds.of(magnitude * 86400); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to seconds."); + System.err.println("Unexpected time unit: " + timeUnit); break; } return Seconds.of(magnitude); @@ -474,40 +364,24 @@ public static Mass parseMass(UnitValueJson unitValueJson) { * @return The {@link Mass} object. */ public static Mass parseMass(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case KG: - case "kgs": - case "kilogram": - case "kilograms": + MassUnit massUnit = MassUnit.fromString(unit); + switch (massUnit) { + case KILOGRAMS: return Kilograms.of(magnitude); - case G: - case "gram": - case "grams": + case GRAMS: return Kilograms.of(magnitude * 0.001); - case MG: - case "milligram": - case "milligrams": + case MILLIGRAMS: return Kilograms.of(magnitude * 0.000001); case TONS: - case "t": - case "ton": return Kilograms.of(magnitude * 1000); - case OZ: - case "ounce": - case "ounces": + case OUNCES: return Ounces.of(magnitude); - case LBS: - case "lb": - case "pound": - case "pounds": + case POUNDS: return Pounds.of(magnitude); - case "st": case STONE: - case "stones": return Kilograms.of(magnitude * 6.35029); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to kilograms."); + System.err.println("Unexpected mass unit: " + massUnit); break; } return Kilograms.of(magnitude); @@ -532,16 +406,16 @@ public static Angle parseAngle(UnitValueJson unitValueJson) { * @return The {@link Angle} object. */ public static Angle parseAngle(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case DEG: - case "degrees": + AngleUnit angleUnit = AngleUnit.fromString(unit); + switch (angleUnit) { + case DEGREES: return Degrees.of(magnitude); - case RAD: - case "radians": + case RADIANS: return Radians.of(magnitude); + case ROTATIONS: + return Rotations.of(magnitude); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to degrees."); + System.err.println("Unexpected angle unit: " + angleUnit); return Degrees.of(magnitude); } } @@ -568,34 +442,18 @@ public static AngularAcceleration parseAngularAcceleration(UnitValueJson unitVal * @return The {@link AngularVelocity} object. */ public static AngularVelocity parseAngularVelocity(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case DEGPS: - case "deg/sec": - case "deg/second": - case "degrees/s": - case "degrees/sec": - case "degrees/second": + AngularVelocityUnit angVelUnit = AngularVelocityUnit.fromString(unit); + switch (angVelUnit) { + case DEGREES_PER_SECOND: return DegreesPerSecond.of(magnitude); - case rpm: - case "RPM": + case REVOLUTIONS_PER_MINUTE: return RPM.of(magnitude); - case rps: - case "RPS": + case ROTATIONS_PER_SECOND: return RotationsPerSecond.of(magnitude); - case RADPS: - case "rad/sec": - case "rad/second": - case "rads/s": - case "rads/sec": - case "rads/second": - case "radians": - case "radians/s": - case "radians/sec": - case "radians/second": + case RADIANS_PER_SECOND: return RadiansPerSecond.of(magnitude); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to degrees/sec."); + System.err.println("Unexpected angular velocity unit: " + angVelUnit); return DegreesPerSecond.of(magnitude); } } @@ -620,41 +478,46 @@ public static AngularVelocity parseAngularVelocity(UnitValueJson unitValueJson) * @return The {@link AngularAcceleration} object. */ public static AngularAcceleration parseAngularAcceleration(double magnitude, String unit) { - switch (unit.trim().toLowerCase()) { - case DEGPS2: - case "deg/s/s": - case "deg/s2": - case "degrees/s/s": - case "degrees/s^2": - case "degrees/s2": + AngularAccelerationUnit angAccelUnit = AngularAccelerationUnit.fromString(unit); + switch (angAccelUnit) { + case DEGREES_PER_SECOND_SQUARED: return DegreesPerSecondPerSecond.of(magnitude); - case RPMPS: - case "RPM/s": - case "RPM/sec": - case "rpm/s": - case "rpm/sec": + case REVOLUTIONS_PER_MINUTE_PER_SECOND: return RPM.of(magnitude).per(Second); - case RPS2: - case "RPS2": - case "RPS/s": - case "RPS/sec": - case "RPS^2": - case "rps2": - case "rps/s": - case "rps/sec": + case ROTATIONS_PER_SECOND_SQUARED: return RotationsPerSecondPerSecond.of(magnitude); - case RADPS2: - case "rads/s/s": - case "rads/s2": - case "radians": - case "radians/s/s": - case "radians/s^2": - case "radians/s2": + case RADIANS_PER_SECOND_SQUARED: return RadiansPerSecondPerSecond.of(magnitude); default: - System.err.println( - "Unknown unit: " + unit + " for " + magnitude + ". Defaulting to degrees/sec^2."); + System.err.println("Unexpected angular acceleration unit: " + angAccelUnit); return DegreesPerSecondPerSecond.of(magnitude); } } + + /** + * Converts a magnitude and a unit into a {@link MomentOfInertia} object. + * + * @param unitValueJson A json object containing the magnitude and unit of the moment of inertia. + * @return The {@link MomentOfInertia} object. + */ + public static MomentOfInertia parseMomentOfInertia(UnitValueJson unitValueJson) { + return parseMomentOfInertia(unitValueJson.getMagnitude(), unitValueJson.getUnit()); + } + + /** + * Converts a magnitude and a unit into a {@link MomentOfInertia} object. + * + * @param magnitude The magnitude of the moment of inertia + * @param unit The unit of the moment moment @return The {@link MomentOfInertia} object + */ + public static MomentOfInertia parseMomentOfInertia(double magnitude, String unit) { + MomentOfInertiaUnit moiUnit = MomentOfInertiaUnit.fromString(unit); + switch (moiUnit) { + case KILOGRAM_SQUARE_METERS: + return KilogramSquareMeters.of(magnitude); + default: + System.err.println("Unexpected moment of inertia unit: " + moiUnit); + return KilogramSquareMeters.of(magnitude); + } + } } diff --git a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java index 1f4cdd85..fbd61265 100644 --- a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java +++ b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java @@ -4,137 +4,203 @@ package org.frc5010.common.config.json; +import static edu.wpi.first.units.Units.Degrees; import static edu.wpi.first.units.Units.KilogramSquareMeters; -import static edu.wpi.first.units.Units.Kilograms; import static edu.wpi.first.units.Units.Meters; +import static edu.wpi.first.units.Units.MetersPerSecond; import static edu.wpi.first.units.Units.Volts; +import com.pathplanner.lib.config.ModuleConfig; +import com.pathplanner.lib.config.RobotConfig; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.wpilibj.RobotBase; -import frc.robot.generated.TunerConstants; import java.io.File; import java.io.IOException; import java.util.Optional; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.config.ConfigConstants; +import org.frc5010.common.config.UnitsParser; +import org.frc5010.common.config.json.devices.DeviceConfigReader; +import org.frc5010.common.config.json.devices.DrivetrainConstantsJson; import org.frc5010.common.constants.RobotConstantsDef; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; import org.frc5010.common.drive.swerve.GenericSwerveDrivetrain; +import org.frc5010.common.drive.swerve.SwerveDriveFunctions; import org.frc5010.common.drive.swerve.akit.AkitSwerveDrive; import org.frc5010.common.drive.swerve.akit.GyroIOPigeon2; import org.frc5010.common.drive.swerve.akit.GyroIOSim; +import org.frc5010.common.drive.swerve.akit.ModuleIOSim; import org.frc5010.common.drive.swerve.akit.ModuleIOSpark; import org.frc5010.common.drive.swerve.akit.ModuleIOSparkTalon; import org.frc5010.common.drive.swerve.akit.ModuleIOTalonFXReal; import org.frc5010.common.drive.swerve.akit.ModuleIOTalonFXSim; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.drivesims.COTS; -import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; -import org.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; -import org.ironmaple.simulation.drivesims.configs.SwerveModuleSimulationConfig; +import org.frc5010.common.drive.swerve.akit.SparkOdometryThread; +import org.frc5010.common.drive.swerve.akit.TalonFXOdometryThread; import org.littletonrobotics.junction.mechanism.LoggedMechanism2d; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.drivesims.COTS; +import swervelib.simulation.ironmaple.simulation.drivesims.SwerveDriveSimulation; +import swervelib.simulation.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; +import swervelib.simulation.ironmaple.simulation.drivesims.configs.SwerveModuleSimulationConfig; -/** Add your docs here. */ +/** Configuration for AKit Swerve Drivetrain */ public class AKitSwerveDrivetrainJson implements DrivetrainPropertiesJson { public String type = "SparkTalon"; - public double robotMassKg = 74.088; - public double robotMOI = 6.883; - public double wheelCOF = 1.2; + public DrivetrainConstantsJson constants; private Optional gamePiecesJson = Optional.empty(); @Override - public void readDrivetrainConfiguration(GenericRobot robot, File directory) throws IOException { - AkitSwerveDrive.mapleSimConfig = + public void readDrivetrainConfiguration(GenericRobot robot, File directory) throws IOException {} + + @Override + public void createDriveTrain(GenericRobot robot) { + SwerveDriveFunctions driveFunctions; + AkitSwerveConfig config; + GenericSwerveDrivetrain drivetrain = null; + config = AkitSwerveConfig.builder(constants, drivetrain); + + RobotConfig PP_CONFIG = + new RobotConfig( + config.getRobotMass(), + config.getDriveInertia(), + new ModuleConfig( + config.FrontLeft.WheelRadius, + config.getMaxDriveSpeed().in(MetersPerSecond), + constants.wheelCOF, + DeviceConfigReader.getSimulatedMotor( + constants.modules.get("frontLeft").driveMotorSetup.motorType, 1) + .withReduction(config.FrontLeft.DriveMotorGearRatio), + config.FrontLeft.SlipCurrent, + 1), + getModuleTranslations(config)); + + SwerveDriveFunctions.mapleSimConfig = DriveTrainSimulationConfig.Default() - .withRobotMass(Kilograms.of(robotMassKg)) - .withCustomModuleTranslations(getModuleTranslations()) + .withBumperSize(config.getBumperFrameWidth(), config.getBumperFrameLength()) + .withRobotMass(config.getRobotMass()) + .withCustomModuleTranslations(getModuleTranslations(config)) .withGyro(COTS.ofPigeon2()) .withSwerveModule( new SwerveModuleSimulationConfig( - DCMotor.getKrakenX60(1), - DCMotor.getFalcon500(1), - TunerConstants.FrontLeft.DriveMotorGearRatio, - TunerConstants.FrontLeft.SteerMotorGearRatio, - Volts.of(TunerConstants.FrontLeft.DriveFrictionVoltage), - Volts.of(TunerConstants.FrontLeft.SteerFrictionVoltage), - Meters.of(TunerConstants.FrontLeft.WheelRadius), - KilogramSquareMeters.of(TunerConstants.FrontLeft.SteerInertia), - wheelCOF)); - } + DeviceConfigReader.getSimulatedMotor( + constants.modules.get("frontLeft").driveMotorSetup.motorType, 1), + DeviceConfigReader.getSimulatedMotor( + constants.modules.get("frontLeft").steerMotorSetup.motorType, 1), + config.getDriveGearRatio(), + config.getSteerGearRatio(), + Volts.of(config.FrontLeft.DriveFrictionVoltage), + Volts.of(config.FrontLeft.SteerFrictionVoltage), + Meters.of(config.FrontLeft.WheelRadius), + KilogramSquareMeters.of(config.FrontLeft.SteerInertia), + constants.wheelCOF)); - @Override - public void createDriveTrain(GenericRobot robot) { - AkitSwerveDrive drive; if (RobotBase.isSimulation()) { - - AkitSwerveDrive.driveSimulation = + SwerveDriveFunctions.driveSimulation = new SwerveDriveSimulation( - AkitSwerveDrive.mapleSimConfig, new Pose2d(3, 3, new Rotation2d())); - SimulatedArena.getInstance().addDriveTrainSimulation(AkitSwerveDrive.driveSimulation); - drive = - new AkitSwerveDrive( - new GyroIOSim(AkitSwerveDrive.driveSimulation.getGyroSimulation()), - new ModuleIOTalonFXSim( - TunerConstants.FrontLeft, AkitSwerveDrive.driveSimulation.getModules()[0]), - new ModuleIOTalonFXSim( - TunerConstants.FrontRight, AkitSwerveDrive.driveSimulation.getModules()[1]), - new ModuleIOTalonFXSim( - TunerConstants.BackLeft, AkitSwerveDrive.driveSimulation.getModules()[2]), - new ModuleIOTalonFXSim( - TunerConstants.BackRight, AkitSwerveDrive.driveSimulation.getModules()[3]), - AkitSwerveDrive.driveSimulation::setSimulationWorldPose); + SwerveDriveFunctions.mapleSimConfig, new Pose2d(3, 3, new Rotation2d())); + SimulatedArena.getInstance().addDriveTrainSimulation(SwerveDriveFunctions.driveSimulation); + + // Initialize odometry frequency for simulation so Phoenix 6 signals registered + // to it (like turnAbsolutePosition after recent commits) actually update. + config.ODOMETRY_FREQUENCY = 250.0; + + if ("TalonFX".equals(type)) { + TalonFXOdometryThread.createInstance(config); + driveFunctions = + new AkitSwerveDrive( + config, + new GyroIOSim(SwerveDriveFunctions.driveSimulation.getGyroSimulation()), + new ModuleIOTalonFXSim( + config, config.FrontLeft, SwerveDriveFunctions.driveSimulation.getModules()[0]), + new ModuleIOTalonFXSim( + config, + config.FrontRight, + SwerveDriveFunctions.driveSimulation.getModules()[1]), + new ModuleIOTalonFXSim( + config, config.BackLeft, SwerveDriveFunctions.driveSimulation.getModules()[2]), + new ModuleIOTalonFXSim( + config, config.BackRight, SwerveDriveFunctions.driveSimulation.getModules()[3]), + SwerveDriveFunctions.driveSimulation::setSimulationWorldPose); + } else { + TalonFXOdometryThread.createInstance(config); + driveFunctions = + new AkitSwerveDrive( + config, + new GyroIOSim(SwerveDriveFunctions.driveSimulation.getGyroSimulation()), + new ModuleIOSim(), + new ModuleIOSim(), + new ModuleIOSim(), + new ModuleIOSim(), + SwerveDriveFunctions.driveSimulation::setSimulationWorldPose); + } } else { + config.ODOMETRY_FREQUENCY = config.getCANBus().isNetworkFD() ? 250.0 : 100.0; if ("SparkTalon".equals(type)) { - drive = + TalonFXOdometryThread.createInstance(config); + SparkOdometryThread.createInstance(); + driveFunctions = new AkitSwerveDrive( - new GyroIOPigeon2(), - new ModuleIOSparkTalon(TunerConstants.FrontLeft), - new ModuleIOSparkTalon(TunerConstants.FrontRight), - new ModuleIOSparkTalon(TunerConstants.BackLeft), - new ModuleIOSparkTalon(TunerConstants.BackRight), + config, + new GyroIOPigeon2(config), + new ModuleIOSparkTalon(config, config.FrontLeft), + new ModuleIOSparkTalon(config, config.FrontRight), + new ModuleIOSparkTalon(config, config.BackLeft), + new ModuleIOSparkTalon(config, config.BackRight), (pose) -> {}); } else if ("Spark".equals(type)) { - drive = + SparkOdometryThread.createInstance(); + driveFunctions = new AkitSwerveDrive( - new GyroIOPigeon2(), + config, + new GyroIOPigeon2(config), new ModuleIOSpark(0), new ModuleIOSpark(1), new ModuleIOSpark(2), new ModuleIOSpark(3), (pose) -> {}); } else if ("TalonFX".equals(type)) { - drive = + TalonFXOdometryThread.createInstance(config); + driveFunctions = new AkitSwerveDrive( - new GyroIOPigeon2(), - new ModuleIOTalonFXReal(TunerConstants.FrontLeft), - new ModuleIOTalonFXReal(TunerConstants.FrontRight), - new ModuleIOTalonFXReal(TunerConstants.BackLeft), - new ModuleIOTalonFXReal(TunerConstants.BackRight), + config, + new GyroIOPigeon2(config), + new ModuleIOTalonFXReal(config, config.FrontLeft), + new ModuleIOTalonFXReal(config, config.FrontRight), + new ModuleIOTalonFXReal(config, config.BackLeft), + new ModuleIOTalonFXReal(config, config.BackRight), (pose) -> {}); } else { throw new IllegalArgumentException("Unknown AkitSwerveDrive type: " + type); } } - GenericSwerveDrivetrain drivetrain = + driveFunctions.setPPRobotConfigSupplier(() -> PP_CONFIG); + drivetrain = new GenericSwerveDrivetrain( new LoggedMechanism2d(RobotConstantsDef.robotVisualH, RobotConstantsDef.robotVisualV), robot.getDrivetrainConstants(), - drive); + driveFunctions); + final GenericSwerveDrivetrain dt = drivetrain; robot.addSubsystem(ConfigConstants.DRIVETRAIN, drivetrain); - robot.setPoseSupplier(() -> drivetrain.getPoseEstimator().getCurrentPose()); - robot.setSimulatedPoseSupplier(() -> drive.getMapleSimPose()); - gamePiecesJson.ifPresent(it -> it.createGamePieces(drivetrain)); + robot.setPoseSupplier(() -> dt.getPoseEstimator().getCurrentPose()); + robot.setSimulatedPoseSupplier(() -> driveFunctions.getSimPose()); + Pose2d startingPoseFromJson = + new Pose2d( + UnitsParser.parseDistance(constants.startingPose.x).in(Meters), + UnitsParser.parseDistance(constants.startingPose.y).in(Meters), + new Rotation2d(UnitsParser.parseAngle(constants.startingPose.rotation).in(Degrees))); + dt.resetPose(startingPoseFromJson); + gamePiecesJson.ifPresent(it -> it.createGamePieces(dt)); } /** Returns an array of module translations. */ - public static Translation2d[] getModuleTranslations() { + public static Translation2d[] getModuleTranslations(AkitSwerveConfig config) { return new Translation2d[] { - new Translation2d(TunerConstants.FrontLeft.LocationX, TunerConstants.FrontLeft.LocationY), - new Translation2d(TunerConstants.FrontRight.LocationX, TunerConstants.FrontRight.LocationY), - new Translation2d(TunerConstants.BackLeft.LocationX, TunerConstants.BackLeft.LocationY), - new Translation2d(TunerConstants.BackRight.LocationX, TunerConstants.BackRight.LocationY) + new Translation2d(config.FrontLeft.LocationX, config.FrontLeft.LocationY), + new Translation2d(config.FrontRight.LocationX, config.FrontRight.LocationY), + new Translation2d(config.BackLeft.LocationX, config.BackLeft.LocationY), + new Translation2d(config.BackRight.LocationX, config.BackRight.LocationY) }; } } diff --git a/src/main/java/org/frc5010/common/config/json/CameraConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/CameraConfigurationJson.java index b19590d6..a10135af 100644 --- a/src/main/java/org/frc5010/common/config/json/CameraConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/CameraConfigurationJson.java @@ -22,7 +22,7 @@ import org.frc5010.common.sensors.camera.PhotonVisionFiducialTargetCamera; import org.frc5010.common.sensors.camera.PhotonVisionPoseCamera; import org.frc5010.common.sensors.camera.PhotonVisionVisualTargetCamera; -import org.frc5010.common.sensors.camera.QuestNav; +import org.frc5010.common.sensors.camera.QuestNavInterface; import org.frc5010.common.sensors.camera.SimulatedCamera; import org.frc5010.common.sensors.camera.SimulatedFiducialTargetCamera; import org.frc5010.common.sensors.camera.SimulatedVisualTargetCamera; @@ -31,54 +31,137 @@ import org.frc5010.common.vision.AprilTags; import org.photonvision.PhotonPoseEstimator.PoseStrategy; -/** Add your docs here. */ +/** + * Configuration data class for camera systems in an FRC robot. + * + *

This class represents the JSON configuration for a single camera, including its type + * (Limelight, PhotonVision, AprilTag), physical pose relative to the robot center, and calibration + * parameters. The configuration supports both real hardware and simulation modes. + * + *

Configuration is typically loaded from JSON files and used to instantiate the appropriate + * camera implementation ({@link LimeLightCamera}, {@link PhotonVisionCamera}, etc.) via the {@link + * #configureCamera(GenericRobot)} method. + */ public class CameraConfigurationJson { - /** Limelight constant */ + /** Constant identifier for Limelight camera type */ public static String LIMELIGHT = "limelight"; - /** PhotonVision constant */ + /** Constant identifier for PhotonVision camera type */ public static String PHOTON_VISION = "photonvision"; - /** AprilTag constant */ + /** Constant identifier for AprilTag detection mode */ public static String APRIL_TAG = "apriltag"; - /** Target camera constant */ + /** Constant identifier for target tracking mode */ public static String TARGET = "target"; - /** Name of the camera */ + /** Unique name of the camera used as a subsystem identifier */ public String name; - /** Use of the camera */ + /** + * The use case for this camera. Valid values include: + * + *

    + *
  • "target" - for target tracking systems + *
  • "apriltag" - for AprilTag-based pose estimation + *
  • "quest" - for QuestNav visual odometry + *
+ */ public String use; - /** Type of the camera */ + /** + * The camera vendor/type. Valid values include: + * + *
    + *
  • "limelight" - Limelight camera + *
  • "photonvision" - PhotonVision camera + *
+ * + * Defaults to "none" if not specified. + */ public String type = "none"; - /** Optional strategy of the camera pose */ + /** + * The pose estimation strategy for PhotonVision cameras (e.g., "AVERAGE_BEST_TARGETS", + * "LOWEST_AMBIGUITY"). Defaults to "none" if not using multi-target pose estimation. + */ public String strategy = "none"; - /** Column in SmartDashboard */ + /** The SmartDashboard column index for this camera's telemetry display */ public int column = 0; - /** Camera X position from center in meters, in the robot's reference frame */ + /** Camera X position offset from robot center in meters (forward/backward in robot frame) */ public double x = 0; - /** Camera Y position from center in meters, in the robot's reference frame */ + /** Camera Y position offset from robot center in meters (left/right in robot frame) */ public double y = 0; - /** Camera Z position from center in meters, in the robot's reference frame */ + /** Camera Z position offset from robot center in meters (up/down in robot frame) */ public double z = 0; - /** Camera roll angle in degrees, in the robot's reference frame */ + /** Camera roll rotation in degrees around the X-axis in the robot's reference frame */ public double roll = 0; - /** Camera pitch angle in degrees, in the robot's reference frame */ + /** Camera pitch rotation in degrees around the Y-axis in the robot's reference frame */ public double pitch = 0; - /** Camera yaw angle in degrees, in the robot's reference frame */ + /** Camera yaw rotation in degrees around the Z-axis in the robot's reference frame */ public double yaw = 0; - /** Camera width in pixels */ + /** Horizontal resolution of the camera in pixels */ public int width = 800; - /** Camera height in pixels */ + /** Vertical resolution of the camera in pixels */ public int height = 600; - /* Camera FOV in degrees */ + /** Camera field of view (FOV) in degrees */ public double fov = 70; - /** optional target height in meters */ + /** Optional height of the target in meters (used for target tracking mode) */ public double targetHeight = 0; - /** optional target fiducial id */ + /** Whether to view game pieces in simulation */ + public boolean viewGamePieces = true; + /** + * Optional array of AprilTag fiducial IDs to track. If empty, all AprilTags may be detected + * depending on the configuration strategy. + */ public int[] targetFiducialIds = new int[0]; /** - * Configures the camera system based on the provided robot. + * Sets whether to view game pieces in simulation. If true, the camera system will simulate vision + * targets for game pieces in the arena. This can be useful for testing vision code without a real + * camera. If false, the camera system will only process vision information for real camera + * images. + * + * @param viewGamePieces whether to enable or disable viewing game pieces in simulation + */ + public void setViewGamePieces(boolean viewGamePieces) { + this.viewGamePieces = viewGamePieces; + } + + /** + * Returns whether the camera system can view game pieces in simulation mode. + * + *

This property is set by the {@link #setViewGamePieces(boolean)} method and controls whether + * the camera system will simulate vision targets for game pieces in the arena. If true, the + * camera system will simulate game pieces in simulation; otherwise, it will only process vision + * information for real camera images. + * + * @return whether the camera system can view game pieces in simulation mode + */ + public boolean canViewGamePieces() { + return viewGamePieces; + } + + /** + * Configures the camera system based on the provided robot and current configuration. + * + *

This method performs the following operations: + * + *

    + *
  1. Creates the appropriate camera instance based on the {@link #type} field (real hardware + * or simulation) + *
  2. Initializes camera parameters (resolution, FOV, pose relative to robot) + *
  3. Sets up the camera according to its configured {@link #use} case: + *
      + *
    • "target" - Creates a target tracking system + *
    • "apriltag" - Registers camera with pose estimator for localization + *
    • "quest" - Initializes QuestNav visual odometry system + *
    + *
  4. Registers the camera/vision system with the robot + *
+ * + *

The method handles both real and simulated robot modes ({@link RobotBase#isReal()}). In + * simulation, cameras are created with physics-based simulation capabilities. * - * @param robot the GenericRobot instance to configure the camera for + * @param robot the {@link GenericRobot} instance to configure the camera for. The robot must have + * a drivetrain subsystem with a pose estimator if using pose-based vision modes + * @throws IllegalArgumentException if the camera type or use case is not recognized + * @throws NullPointerException if required subsystems (e.g., drivetrain) are not available for + * the specified use case */ public void configureCamera(GenericRobot robot) { GenericCamera camera = null; @@ -116,7 +199,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.valueOf(strategy), robotToCamera, robot.getPoseSupplier(), targetFiducialIdList); @@ -126,7 +208,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.valueOf(strategy), robotToCamera, robot.getPoseSupplier()); } @@ -140,7 +221,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.valueOf(strategy), robotToCamera, robot.getPoseSupplier(), targetFiducialIdList); @@ -178,7 +258,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.valueOf(strategy), robotToCamera, robot.getSimulatedPoseSupplier(), width, @@ -195,7 +274,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.LOWEST_AMBIGUITY, robotToCamera, robot.getSimulatedPoseSupplier(), targetFiducialIdList, @@ -208,7 +286,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.LOWEST_AMBIGUITY, robotToCamera, robot.getSimulatedPoseSupplier(), width, @@ -220,7 +297,6 @@ public void configureCamera(GenericRobot robot) { name, column, AprilTags.aprilTagFieldLayout, - PoseStrategy.LOWEST_AMBIGUITY, robotToCamera, robot.getSimulatedPoseSupplier(), width, @@ -228,6 +304,9 @@ public void configureCamera(GenericRobot robot) { fov); } } + if (null != camera) { + camera.setCanViewGamePieces(viewGamePieces); + } switch (use) { case "target": { @@ -251,8 +330,8 @@ public void configureCamera(GenericRobot robot) { } case "quest": { - QuestNav questNav = new QuestNav(robotToCamera); - questNav.resetPose(); + QuestNavInterface questNav = new QuestNavInterface(robotToCamera); + if (drivetrain != null) { // FIX: Undo this questNav.withRobotSpeedSupplier( diff --git a/src/main/java/org/frc5010/common/config/json/GamePiecesJson.java b/src/main/java/org/frc5010/common/config/json/GamePiecesJson.java index 9def035a..3a661008 100644 --- a/src/main/java/org/frc5010/common/config/json/GamePiecesJson.java +++ b/src/main/java/org/frc5010/common/config/json/GamePiecesJson.java @@ -6,8 +6,8 @@ import edu.wpi.first.math.geometry.Translation2d; import org.frc5010.common.drive.swerve.GenericSwerveDrivetrain; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.seasonspecific.crescendo2024.CrescendoNoteOnField; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.seasonspecific.crescendo2024.CrescendoNoteOnField; /** Json class for game piece configurations */ public class GamePiecesJson { diff --git a/src/main/java/org/frc5010/common/config/json/VisionPropertiesJson.java b/src/main/java/org/frc5010/common/config/json/VisionPropertiesJson.java index 152d3231..42d0c003 100644 --- a/src/main/java/org/frc5010/common/config/json/VisionPropertiesJson.java +++ b/src/main/java/org/frc5010/common/config/json/VisionPropertiesJson.java @@ -14,8 +14,8 @@ import java.util.Map; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.vision.AprilTags; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.gamepieces.GamePieceOnFieldSimulation; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.gamepieces.GamePieceOnFieldSimulation; /** JSON class with an array of cameras to configure */ public class VisionPropertiesJson { @@ -25,6 +25,7 @@ public class VisionPropertiesJson { public String aprilTagLayout = "default"; public String simulatedField = "default"; public Map gamePieces = new HashMap<>(); + public boolean viewGamePieces = true; /** * Creates cameras for a given robot using the provided map of camera configurations. @@ -103,6 +104,7 @@ public Map readCameraSystem(File directory) thr assert cameraFile.exists(); CameraConfigurationJson camera = new ObjectMapper().readValue(cameraFile, CameraConfigurationJson.class); + camera.setViewGamePieces(viewGamePieces); camerasMap.put(camera.name, camera); } return camerasMap; diff --git a/src/main/java/org/frc5010/common/config/json/YAGSLDrivetrainJson.java b/src/main/java/org/frc5010/common/config/json/YAGSLDrivetrainJson.java index c78f7695..67afe658 100644 --- a/src/main/java/org/frc5010/common/config/json/YAGSLDrivetrainJson.java +++ b/src/main/java/org/frc5010/common/config/json/YAGSLDrivetrainJson.java @@ -89,7 +89,7 @@ public void createDriveTrain(GenericRobot robot) { yagsl); robot.addSubsystem(ConfigConstants.DRIVETRAIN, drivetrain); robot.setPoseSupplier(() -> drivetrain.getPoseEstimator().getCurrentPose()); - robot.setSimulatedPoseSupplier(() -> yagsl.getMapleSimPose()); + robot.setSimulatedPoseSupplier(() -> yagsl.getSimPose()); gamePiecesJson.ifPresent(it -> it.createGamePieces(drivetrain)); } } diff --git a/src/main/java/org/frc5010/common/config/json/devices/ArmParser.java b/src/main/java/org/frc5010/common/config/json/devices/ArmParser.java index 993a408f..cff5ec7f 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/ArmParser.java +++ b/src/main/java/org/frc5010/common/config/json/devices/ArmParser.java @@ -20,9 +20,6 @@ public class ArmParser { * @param filename the name of the configuration file to read * @param system the system to add the device to * @return the parsed Arm, or null if the file cannot be read or parsed - * @throws StreamReadException if the file cannot be read - * @throws DatabindException if the file cannot be parsed - * @throws IOException if there is an error reading the file */ public static Arm parse(String subDirectory, String filename, SubsystemBase system) { try { diff --git a/src/main/java/org/frc5010/common/config/json/devices/DeviceConfigReader.java b/src/main/java/org/frc5010/common/config/json/devices/DeviceConfigReader.java index bcbf1f37..73b0bf97 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/DeviceConfigReader.java +++ b/src/main/java/org/frc5010/common/config/json/devices/DeviceConfigReader.java @@ -9,11 +9,11 @@ import java.util.Optional; import org.frc5010.common.arch.GenericSubsystem; import org.frc5010.common.config.ConfigConstants; -import org.frc5010.common.config.json.devices.ReflectionsManager.SparkBaseType; import org.frc5010.common.config.json.devices.ReflectionsManager.VENDOR; import org.frc5010.common.motors.GenericMotorController; import org.frc5010.common.motors.MotorConstants.Motor; import org.frc5010.common.motors.MotorFactory; +import org.frc5010.common.motors.hardware.GenericRevBrushlessMotor; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; import yams.motorcontrollers.SmartMotorFactory; @@ -34,6 +34,9 @@ public static GenericMotorController getMotor(String controller, String type, in case "spark": motor = MotorFactory.Spark(id, Motor.valueOf(type)); break; + case "sparkflex": + motor = MotorFactory.SparkFlex(id, Motor.valueOf(type)); + break; case "talonfx": motor = MotorFactory.TalonFX(id, Motor.valueOf(type)); break; @@ -102,6 +105,7 @@ public static void readDeviceConfig(GenericSubsystem system, File deviceFile, St yamsShooterConfigurationJson.configure(system)); break; default: + System.out.println("Unknown device key: " + key); break; } } @@ -118,11 +122,21 @@ public static void readDeviceConfig(GenericSubsystem system, File deviceFile, St * @return {@link SmartMotorController} wrapper for the motor controller. */ public static Optional getSmartMotor( - String controller, String type, int id, SmartMotorControllerConfig config) { + String controller, String type, int id, SmartMotorControllerConfig config, String canbus) { Optional motor = Optional.empty(); DCMotor motorSim = null; int numberOfMotors = config.getFollowers().map(it -> it.length + 1).orElse(1); - Object motorController = getMotor(controller, type, id); + Object motorController = getReflectedGenericMotor(controller, id, canbus); + motorSim = getSimulatedMotor(type, numberOfMotors); + + motor = + SmartMotorFactory.create( + ((GenericMotorController) motorController).getMotor(), motorSim, config); + return motor; + } + + public static DCMotor getSimulatedMotor(String type, int numberOfMotors) { + DCMotor motorSim = null; switch (type.toLowerCase()) { case "neo": motorSim = DCMotor.getNEO(numberOfMotors); @@ -130,19 +144,21 @@ public static Optional getSmartMotor( case "neo550": motorSim = DCMotor.getNeo550(numberOfMotors); break; + case "neovortex": + motorSim = DCMotor.getNeoVortex(numberOfMotors); + break; case "krakenx60": motorSim = DCMotor.getKrakenX60(numberOfMotors); break; case "krakenx60foc": motorSim = DCMotor.getKrakenX60Foc(numberOfMotors); break; + case "krakenx44": + motorSim = DCMotor.getKrakenX44(numberOfMotors); + break; default: } - - motor = - SmartMotorFactory.create( - ((SmartMotorController) motorController).getMotorController(), motorSim, config); - return motor; + return motorSim; } /** @@ -150,42 +166,42 @@ public static Optional getSmartMotor( * * @param controller The type of motor controller, e.g. "spark", "talonfx", "nova", "thrifty", * "thrifty_nova", or "thriftynova" - * @param type The type of motor, e.g. "neo", "neo550", "krakenx60", or "krakenx44" * @param id The CAN ID of the motor controller + * @param canbus The CAN bus of the motor controller * @return The motor controller object */ - public static Object getReflectedMotor(String controller, String type, int id) { + public static Object getReflectedGenericMotor(String controller, int id, String canbus) { ReflectionsManager.VENDOR vendorType = VENDOR.REV; switch (controller.toLowerCase()) { case "spark": case "sparkmax": vendorType = VENDOR.REV; - return ReflectionsManager.create( + return ReflectionsManager.create( vendorType, - "yams.motorcontrollers.local.SparkWrapper", - new Class[] {int.class, SparkBaseType.class}, - new Object[] {id, SparkBaseType.SPARK_MAX}); + "org.frc5010.common.motors.hardware.GenericRevBrushlessMotor", + new Class[] {int.class, boolean.class}, + new Object[] {id, true}); case "sparkflex": vendorType = VENDOR.REV; - return ReflectionsManager.create( + return ReflectionsManager.create( vendorType, - "yams.motorcontrollers.local.SparkWrapper", - new Class[] {int.class, SparkBaseType.class}, - new Object[] {id, SparkBaseType.SPARK_FLEX}); + "org.frc5010.common.motors.hardware.GenericRevBrushlessMotor", + new Class[] {int.class, boolean.class}, + new Object[] {id, false}); case "talonfx": vendorType = VENDOR.PHOENIX5; return ReflectionsManager.create( vendorType, - "yams.motorcontrollers.remote.TalonFXWrapper", - new Class[] {int.class}, - new Object[] {id}); + "org.frc5010.common.motors.hardware.GenericTalonFXMotor", + new Class[] {int.class, String.class}, + new Object[] {id, canbus}); case "talonfxs": vendorType = VENDOR.PHOENIX6; return ReflectionsManager.create( vendorType, - "yams.motorcontrollers.remote.TalonFXSWrapper", - new Class[] {int.class}, - new Object[] {id}); + "org.frc5010.common.motors.hardware.GenericTalonFXSMotor", + new Class[] {int.class, String.class}, + new Object[] {id, canbus}); case "nova": case "thrifty": case "thrifty_nova": @@ -193,7 +209,7 @@ public static Object getReflectedMotor(String controller, String type, int id) { vendorType = VENDOR.THRIFTYBOT; return ReflectionsManager.create( vendorType, - "yams.motorcontrollers.local.NovaWrapper", + "org.frc5010.common.motors.hardware.GenericThriftyNovaMotor", new Class[] {int.class}, new Object[] {id}); default: diff --git a/src/main/java/org/frc5010/common/config/json/devices/DrivetrainConstantsJson.java b/src/main/java/org/frc5010/common/config/json/devices/DrivetrainConstantsJson.java new file mode 100644 index 00000000..9df8a9a3 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/json/devices/DrivetrainConstantsJson.java @@ -0,0 +1,77 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.config.json.devices; + +import java.util.Map; +import org.frc5010.common.config.json.Pose2dJson; +import org.frc5010.common.config.json.UnitValueJson; + +/** SwerveConstantsJson class holds configuration constants for a swerve drive system. */ +public class DrivetrainConstantsJson { + /** The track width */ + public UnitValueJson trackWidth = new UnitValueJson(20, "inches"); + /** The wheel base */ + public UnitValueJson wheelBase = new UnitValueJson(20, "inches"); + /** The diameter of the wheel */ + public UnitValueJson wheelDiameter = new UnitValueJson(4, "inches"); + /** The width of the frame of the robot including bumpers */ + public UnitValueJson bumperFrameWidth = new UnitValueJson(25, "inches"); + /** The length of the frame of the robot including bumpers */ + public UnitValueJson bumperFrameLength = new UnitValueJson(25, "inches"); + /** The maximum drive speed of the robot */ + public UnitValueJson maxDriveSpeed = new UnitValueJson(5, "m/sec"); + /** The settings for the gyro */ + public GyroSettingsConfigurationJson gyro = new GyroSettingsConfigurationJson(); + /** The gear ratio of the drive motor */ + public String driveGearRatio = "1:1"; + /** The gear ratio of the steer motor */ + public String steerGearRatio = "1:1"; + /** The system ID values for the drive motor */ + public MotorSystemIdJson driveMotorControl; + /** The system ID values for the steer motor */ + public MotorSystemIdJson steerMotorControl; + /** + * The module configurations, mapped by module name which should be frontLeft, frontRight, + * backLeft, backRight + */ + public Map modules; + /** + * The ratio of the drive motor to the steer motor. Every 1 rotation of the azimuth results in + * kCoupleRatio drive motor turns; + */ + public double coupleRatio = 1.0; + /** Whether the left side should be inverted */ + public boolean invertLeftSide = true; + /** Whether the right side should be inverted */ + public boolean invertRightSide = true; + /** The steer inertia constant */ + public UnitValueJson steerInertia = new UnitValueJson(0.001, "kg*m^2"); + /** The drive inertia constant */ + public UnitValueJson driveInertia = new UnitValueJson(0.001, "kg*m^2"); + /** The mass of the robot */ + public UnitValueJson robotMass = new UnitValueJson(50, "kg"); + /** The coefficient of friction of the wheels */ + public double wheelCOF = 1.2; + /** The current limit of the drive motors */ + public UnitValueJson slipCurrent = new UnitValueJson(40, "amps"); + /** The CAN bus for the drivetrain */ + public String canbus = ""; + /** Starting pose of the robot */ + public Pose2dJson startingPose = new Pose2dJson(); + + /** Configuration for a swerve module */ + public static class ModuleConfigJson { + /** The motor setup for the drive motor */ + public MotorSetupJson driveMotorSetup = new MotorSetupJson(); + /** The motor setup for the steer motor */ + public MotorSetupJson steerMotorSetup = new MotorSetupJson(); + /** The encoder ID */ + public int encoderId = 2; + /** The absolute encoder offset */ + public UnitValueJson absoluteOffset = new UnitValueJson(0, "degrees"); + /** Whether the encoder is inverted */ + public boolean encoderInverted = false; + } +} diff --git a/src/main/java/org/frc5010/common/config/json/devices/GyroSettingsConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/GyroSettingsConfigurationJson.java index cc6b9f59..d39f3b69 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/GyroSettingsConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/GyroSettingsConfigurationJson.java @@ -13,13 +13,16 @@ public class GyroSettingsConfigurationJson implements DeviceConfiguration { public String type; /** The ID of the gyro sensor, if needed */ public int id; + /** Whether the gyro sensor is inverted */ + public boolean inverted = false; + /** The CAN bus the gyro is connected to */ + public String canbus = ""; /** - * Configures and returns an appropriate gyro sensor based on the specified type. + * Configures a gyro sensor based on the specified type. * - * @param mechanismSimulation The LoggedMechanism2d instance for visualization, if needed. - * @return An instance of a gyro sensor (NavXGyro or PigeonGyro) based on the type, or null if the - * type is unrecognized. + * @param deviceHandler the GenericSubsystem to configure + * @return the configured gyro sensor, or null if the type is not recognized */ @Override public Object configure(GenericSubsystem deviceHandler) { diff --git a/src/main/java/org/frc5010/common/config/json/devices/MotorSetupJson.java b/src/main/java/org/frc5010/common/config/json/devices/MotorSetupJson.java index 3c66f236..978cd05e 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/MotorSetupJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/MotorSetupJson.java @@ -5,9 +5,9 @@ package org.frc5010.common.config.json.devices; import edu.wpi.first.math.Pair; -import org.frc5010.common.config.UnitsParser; import org.frc5010.common.config.json.Translation3dJson; import org.frc5010.common.config.json.UnitValueJson; +import org.frc5010.common.config.units.CurrentUnit; import org.frc5010.common.motors.GenericMotorController; import yams.mechanisms.config.MechanismPositionConfig; import yams.motorcontrollers.SmartMotorControllerConfig; @@ -24,9 +24,12 @@ public static class FollowerMotorJson { public String motorType; public String controllerType; public int canId; + /** The CAN bus for the motor */ + public String canBus = ""; + public FollowerMotorJson[] followers = new FollowerMotorJson[0]; public String idleMode = "BRAKE"; - public UnitValueJson currentLimit = new UnitValueJson(40, UnitsParser.AMPS); + public UnitValueJson currentLimit = new UnitValueJson(40, CurrentUnit.AMPS.toString()); public boolean inverted = false; public int numberOfMotors = 1; public Translation3dJson robotToMotor = new Translation3dJson(); diff --git a/src/main/java/org/frc5010/common/config/json/devices/MotorSystemIdJson.java b/src/main/java/org/frc5010/common/config/json/devices/MotorSystemIdJson.java index f78fdcf2..b29fc49c 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/MotorSystemIdJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/MotorSystemIdJson.java @@ -4,10 +4,12 @@ package org.frc5010.common.config.json.devices; -import org.frc5010.common.config.UnitsParser; import org.frc5010.common.config.json.UnitValueJson; +import org.frc5010.common.config.units.LinearAccelerationUnit; +import org.frc5010.common.config.units.LinearVelocityUnit; +import org.frc5010.common.config.units.TimeUnit; -/** Add your docs here. */ +/** System identification parameters for a motor. */ public class MotorSystemIdJson { public static class FeedBack { public double p = 0; @@ -22,11 +24,13 @@ public static class FeedForward { public double a = 0; } - public UnitValueJson closedLoopRamp = new UnitValueJson(0.25, UnitsParser.SEC); - public UnitValueJson openLoopRamp = new UnitValueJson(0.25, UnitsParser.SEC); + public UnitValueJson closedLoopRamp = new UnitValueJson(0.25, TimeUnit.SECONDS.toString()); + public UnitValueJson openLoopRamp = new UnitValueJson(0.25, TimeUnit.SECONDS.toString()); public FeedBack feedBack; - public UnitValueJson maxVelocity = new UnitValueJson(0, UnitsParser.MPS); - public UnitValueJson maxAcceleration = new UnitValueJson(0, UnitsParser.MPS2); + public UnitValueJson maxVelocity = + new UnitValueJson(0, LinearVelocityUnit.METERS_PER_SECOND.toString()); + public UnitValueJson maxAcceleration = + new UnitValueJson(0, LinearAccelerationUnit.METERS_PER_SECOND_SQUARED.toString()); public FeedForward feedForward = new FeedForward(); public String controlMode = "CLOSED_LOOP"; } diff --git a/src/main/java/org/frc5010/common/config/json/devices/PercentMotorConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/PercentMotorConfigurationJson.java index cd077642..c8c77ddf 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/PercentMotorConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/PercentMotorConfigurationJson.java @@ -29,10 +29,12 @@ public class PercentMotorConfigurationJson implements DeviceConfiguration { public double z; /** The logging level */ public String logLevel = "COMPETITION"; + /** * Configures a PercentControlMotor with the given parameters and visualizes it. * - * @param mechanismSimulation The LoggedMechanism2d instance for visualization. + * @param deviceHandler The GenericSubsystem from which to get the display values helper and + * mechanism visualizer. * @return A configured PercentControlMotor object. */ @Override diff --git a/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java b/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java index 7b24bac4..85eb899f 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java +++ b/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java @@ -14,7 +14,8 @@ public enum VENDOR { PHOENIX5, PHOENIX6, /** ThriftyLib */ - THRIFTYBOT + THRIFTYBOT, + YAMS } public enum SparkBaseType { @@ -26,9 +27,10 @@ public enum SparkBaseType { new HashMap() { { put(VENDOR.REV, "com.revrobotics.spark.SparkBase"); - put(VENDOR.PHOENIX5, "com.ctre.phoenix.hardware.TalonSRX"); - put(VENDOR.PHOENIX6, "com.ctre.phoenix6.hardware.TalonFX"); - put(VENDOR.THRIFTYBOT, "com.thriftybot.hardware.ThriftyBot"); + put(VENDOR.PHOENIX5, "com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX"); + put(VENDOR.PHOENIX6, "com.ctre.phoenix6.hardware.TalonFXS"); + put(VENDOR.THRIFTYBOT, "com.thethriftybot.ThriftyNova"); + put(VENDOR.YAMS, "yams.motorcontrollers.SmartMotorController"); } }; @@ -41,8 +43,14 @@ public enum SparkBaseType { public static boolean checkIfVendorLibExists(VENDOR vendor) { try { // If the class is found, the library exists - Class.forName(vendorLibs.get(vendor)); + if (vendorLibs.containsKey(vendor)) { + Class.forName(vendorLibs.get(vendor)); + } else { + System.err.println("Vendor " + vendor + " not recognized!"); + return false; + } } catch (Exception e) { + System.err.println("Vendor " + vendor + " class not found: " + vendorLibs.get(vendor) + ". "); return false; } return true; diff --git a/src/main/java/org/frc5010/common/config/json/devices/SubsystemJson.java b/src/main/java/org/frc5010/common/config/json/devices/SubsystemJson.java index 04f298d4..2d8d005b 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/SubsystemJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/SubsystemJson.java @@ -4,13 +4,19 @@ import com.fasterxml.jackson.databind.DatabindException; import java.io.File; import java.io.IOException; -import java.util.Map; +import java.util.List; import org.frc5010.common.arch.GenericSubsystem; /** The base JSON class for subsystem configurations */ public class SubsystemJson { + public static class DeviceEntry { + public String device; + public String file; + + public DeviceEntry() {} + } /** A map of device names and device configuration file names */ - public Map devices; + public List devices; /** Whether to display the subsystem in the dashboard */ public boolean display = false; /** The logging level for the robot */ @@ -28,10 +34,10 @@ public class SubsystemJson { */ public void configureSubsystem(GenericSubsystem system, File directory) throws StreamReadException, DatabindException, IOException { - for (String key : devices.keySet()) { - File deviceFile = new File(directory, devices.get(key)); + for (DeviceEntry key : devices) { + File deviceFile = new File(directory, key.file); assert deviceFile.exists(); - DeviceConfigReader.readDeviceConfig(system, deviceFile, key); + DeviceConfigReader.readDeviceConfig(system, deviceFile, key.device); } } } diff --git a/src/main/java/org/frc5010/common/config/json/devices/VelocityMotorConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/VelocityMotorConfigurationJson.java index 948023d0..d4ee6a78 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/VelocityMotorConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/VelocityMotorConfigurationJson.java @@ -49,7 +49,8 @@ public class VelocityMotorConfigurationJson implements DeviceConfiguration { /** * Configures a VelocityControlMotor with the given parameters and visualizes it. * - * @param mechanismSimulation The LoggedMechanism2d instance for visualization. + * @param deviceHandler The GenericSubsystem from which to get the display values helper and + * mechanism visualizer. * @return A configured VelocityControlMotor object. */ @Override diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsArmConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsArmConfigurationJson.java index 60326cec..0c9df9d4 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsArmConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsArmConfigurationJson.java @@ -6,33 +6,37 @@ import edu.wpi.first.math.controller.ArmFeedforward; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import java.util.Optional; +import org.frc5010.common.config.ConfigConstants.ControlAlgorithm; import org.frc5010.common.config.DeviceConfiguration; import org.frc5010.common.config.UnitsParser; import org.frc5010.common.config.json.UnitValueJson; -import org.frc5010.common.motors.GenericMotorController; -import yams.gearing.GearBox; -import yams.gearing.MechanismGearing; +import org.frc5010.common.config.units.AngleUnit; +import org.frc5010.common.config.units.DistanceUnit; +import org.frc5010.common.config.units.MassUnit; +import org.frc5010.common.config.units.VoltageUnit; import yams.mechanisms.config.ArmConfig; import yams.mechanisms.positional.Arm; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; -import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; public class YamsArmConfigurationJson implements DeviceConfiguration { public MotorSetupJson motorSetup = new MotorSetupJson(); + public ControlAlgorithm controlAlgorithm = ControlAlgorithm.SIMPLE; public MotorSystemIdJson motorSystemId = new MotorSystemIdJson(); - public UnitValueJson length = new UnitValueJson(0, UnitsParser.IN); - public UnitValueJson lowerHardLimit = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson upperHardLimit = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson startingAngle = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson lowerSoftLimit = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson upperSoftLimit = new UnitValueJson(0, UnitsParser.DEG); + public MotorSystemIdJson simSystemId = new MotorSystemIdJson(); + public UnitValueJson length = new UnitValueJson(0, DistanceUnit.INCHES.toString()); + public UnitValueJson lowerHardLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson upperHardLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson startingAngle = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson lowerSoftLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson upperSoftLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); public double[] gearing; - public UnitValueJson mass = new UnitValueJson(0, UnitsParser.LBS); - public UnitValueJson voltageCompensation = new UnitValueJson(12, UnitsParser.VOLTS); - public UnitValueJson horizontalZero = new UnitValueJson(0, UnitsParser.DEG); + public String gearStages = ""; + public UnitValueJson mass = new UnitValueJson(0, MassUnit.POUNDS.toString()); + public UnitValueJson voltageCompensation = new UnitValueJson(12, VoltageUnit.VOLTS.toString()); + public UnitValueJson horizontalZero = new UnitValueJson(0, AngleUnit.DEGREES.toString()); /** * Configure the given GenericSubsystem with an arm using the given json configuration. @@ -42,48 +46,49 @@ public class YamsArmConfigurationJson implements DeviceConfiguration { */ @Override public Arm configure(SubsystemBase deviceHandler) { - GenericMotorController motor = - DeviceConfigReader.getMotor( - motorSetup.controllerType, motorSetup.motorType, motorSetup.canId); - SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(deviceHandler) - .withClosedLoopController( - motorSystemId.feedBack.p, - motorSystemId.feedBack.i, - motorSystemId.feedBack.d, - UnitsParser.parseAngularVelocity(motorSystemId.maxVelocity), - UnitsParser.parseAngularAcceleration(motorSystemId.maxAcceleration)) .withSoftLimit( UnitsParser.parseAngle(lowerSoftLimit), UnitsParser.parseAngle(upperSoftLimit)) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(gearing))) - .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) - .withTelemetry( - motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) - .withMotorInverted(motorSetup.inverted) - .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)) - .withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)) .withFeedforward( new ArmFeedforward( motorSystemId.feedForward.s, motorSystemId.feedForward.g, motorSystemId.feedForward.v, motorSystemId.feedForward.a)) - .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); - MotorSetupJson.setupFollowers(motorConfig, motorSetup); - motor.setMotorSimulationType( - motor.getMotorConfig().getMotorSimulationType(motorSetup.numberOfMotors)); + .withSimFeedforward( + new ArmFeedforward( + simSystemId.feedForward.s, + simSystemId.feedForward.g, + simSystemId.feedForward.v, + simSystemId.feedForward.a)); + + YamsConfigCommon.PhysicalParameters physicalParams = + new YamsConfigCommon.PhysicalParameters( + voltageCompensation, mass, length, gearing, gearStages); + + Optional smartMotor = + YamsConfigCommon.configureSmartMotorController( + motorSetup, motorConfig, controlAlgorithm, motorSystemId, simSystemId, physicalParams); + if (smartMotor.isEmpty()) { + throw new RuntimeException( + "Smart motor configuration issue. ID: " + + motorSetup.canId + + " Controller: " + + motorSetup.controllerType + + " Motor: " + + motorSetup.motorType); + } - SmartMotorController smartMotor = motor.getSmartMotorController(motorConfig); ArmConfig armConfig = - new ArmConfig(smartMotor) - .withLength(UnitsParser.parseDistance(length)) + new ArmConfig(smartMotor.get()) + .withLength(physicalParams.length) .withHardLimit( UnitsParser.parseAngle(lowerHardLimit), UnitsParser.parseAngle(upperHardLimit)) .withTelemetry(motorSetup.name, TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withMass(UnitsParser.parseMass(mass)) + .withMass(physicalParams.mass) .withStartingPosition(UnitsParser.parseAngle(startingAngle)) + .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) .withHorizontalZero(UnitsParser.parseAngle(horizontalZero)); Arm arm = new Arm(armConfig); return arm; diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java b/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java new file mode 100644 index 00000000..314728b8 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java @@ -0,0 +1,159 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.config.json.devices; + +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.math.trajectory.ExponentialProfile; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.Mass; +import edu.wpi.first.units.measure.Voltage; +import frc.robot.Robot; +import java.util.Optional; +import org.frc5010.common.config.ConfigConstants.ControlAlgorithm; +import org.frc5010.common.config.UnitsParser; +import org.frc5010.common.config.json.UnitValueJson; +import org.frc5010.common.motors.GenericMotorController; +import yams.gearing.GearBox; +import yams.gearing.MechanismGearing; +import yams.math.ExponentialProfilePIDController; +import yams.motorcontrollers.SmartMotorController; +import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; +import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; +import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; +import yams.motorcontrollers.SmartMotorFactory; + +/** Add your docs here. */ +public class YamsConfigCommon { + public static class PhysicalParameters { + public Voltage voltageCompensation; + public Mass mass; + public Distance length; + public MechanismGearing gearing; + + public PhysicalParameters( + UnitValueJson voltageCompensation, + UnitValueJson mass, + UnitValueJson length, + double[] gearing, + String gearStages) { + this.voltageCompensation = UnitsParser.parseVolts(voltageCompensation); + this.mass = UnitsParser.parseMass(mass); + this.length = UnitsParser.parseDistance(length); + if (!gearStages.isEmpty()) { + this.gearing = new MechanismGearing(GearBox.fromStages(gearStages)); + } else { // gearStages is empty, so use gearing array + this.gearing = new MechanismGearing(GearBox.fromReductionStages(gearing)); + } + } + } + + public static Optional configureSmartMotorController( + MotorSetupJson motorSetup, + SmartMotorControllerConfig motorConfig, + ControlAlgorithm controlAlgorithm, + MotorSystemIdJson motorSystemId, + MotorSystemIdJson simSystemId, + PhysicalParameters physicalParams) { + + MotorSetupJson.setupFollowers(motorConfig, motorSetup); + int numberOfMotors = motorConfig.getFollowers().map(it -> it.length + 1).orElse(1); + Object motorController = + DeviceConfigReader.getReflectedGenericMotor( + motorSetup.controllerType, motorSetup.canId, motorSetup.canBus); + DCMotor motorSim = DeviceConfigReader.getSimulatedMotor(motorSetup.motorType, numberOfMotors); + + if (Robot.isSimulation() && !ControlAlgorithm.SIMPLE.equals(controlAlgorithm)) { + controlAlgorithm = ControlAlgorithm.SIMPLE; + } + switch (controlAlgorithm) { + case PROFILED: + { + motorConfig + .withClosedLoopController( + motorSystemId.feedBack.p, + motorSystemId.feedBack.i, + motorSystemId.feedBack.d, + UnitsParser.parseAngularVelocity(motorSystemId.maxVelocity), + UnitsParser.parseAngularAcceleration(motorSystemId.maxAcceleration)) + .withSimClosedLoopController( + simSystemId.feedBack.p, + simSystemId.feedBack.i, + simSystemId.feedBack.d, + UnitsParser.parseAngularVelocity(simSystemId.maxVelocity), + UnitsParser.parseAngularAcceleration(simSystemId.maxAcceleration)); + break; + } + case EXPO: + case EXPO_ELEVATOR: + { + ExponentialProfile.Constraints constraints; + if (controlAlgorithm == ControlAlgorithm.EXPO_ELEVATOR) { + constraints = + ExponentialProfilePIDController.createElevatorConstraints( + physicalParams.voltageCompensation, + motorSim, + physicalParams.mass, + physicalParams.length, + physicalParams.gearing); + } else { + constraints = + ExponentialProfilePIDController.createArmConstraints( + physicalParams.voltageCompensation, + motorSim, + physicalParams.mass, + physicalParams.length, + physicalParams.gearing); + } + ExponentialProfilePIDController controller = + new ExponentialProfilePIDController( + motorSystemId.feedBack.p, + motorSystemId.feedBack.i, + motorSystemId.feedBack.d, + constraints); + ExponentialProfilePIDController simController = + new ExponentialProfilePIDController( + simSystemId.feedBack.p, + simSystemId.feedBack.i, + simSystemId.feedBack.d, + constraints); + motorConfig + .withClosedLoopController(controller) + .withSimClosedLoopController(simController); + break; + } + case SIMPLE: + default: + { + motorConfig + .withClosedLoopController( + motorSystemId.feedBack.p, motorSystemId.feedBack.i, motorSystemId.feedBack.d) + .withSimClosedLoopController( + simSystemId.feedBack.p, simSystemId.feedBack.i, simSystemId.feedBack.d); + break; + } + } + motorConfig + .withGearing(physicalParams.gearing) + .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)) + .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)) + .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) + .withTelemetry(motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) + .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) + .withMotorInverted(motorSetup.inverted); + + switch (motorSetup.controllerType.toLowerCase()) { + case "thrifty": + case "nova": + case "thriftynova": + case "thrifty_nova": + default: + motorConfig.withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)); + break; + } + return SmartMotorFactory.create( + ((GenericMotorController) motorController).getMotor(), motorSim, motorConfig); + } +} diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsElevatorConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsElevatorConfigurationJson.java index 14153f96..b36a7099 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsElevatorConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsElevatorConfigurationJson.java @@ -5,33 +5,36 @@ import edu.wpi.first.math.controller.ElevatorFeedforward; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import java.util.Optional; +import org.frc5010.common.config.ConfigConstants.ControlAlgorithm; import org.frc5010.common.config.DeviceConfiguration; import org.frc5010.common.config.UnitsParser; import org.frc5010.common.config.json.UnitValueJson; -import org.frc5010.common.motors.GenericMotorController; -import yams.gearing.GearBox; -import yams.gearing.MechanismGearing; +import org.frc5010.common.config.units.DistanceUnit; +import org.frc5010.common.config.units.MassUnit; +import org.frc5010.common.config.units.VoltageUnit; import yams.mechanisms.config.ElevatorConfig; import yams.mechanisms.positional.Elevator; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; -import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; public class YamsElevatorConfigurationJson implements DeviceConfiguration { public MotorSetupJson motorSetup = new MotorSetupJson(); + public ControlAlgorithm controlAlgorithm = ControlAlgorithm.SIMPLE; public MotorSystemIdJson motorSystemId = new MotorSystemIdJson(); + public MotorSystemIdJson simSystemId = new MotorSystemIdJson(); public int sprocketTeeth = 0; - public UnitValueJson drumRadius = new UnitValueJson(0, UnitsParser.IN); - public UnitValueJson lowerSoftLimit = new UnitValueJson(0, UnitsParser.M); - public UnitValueJson upperSoftLimit = new UnitValueJson(0, UnitsParser.M); - public UnitValueJson lowerHardLimit = new UnitValueJson(0, UnitsParser.M); - public UnitValueJson upperHardLimit = new UnitValueJson(0, UnitsParser.M); + public UnitValueJson drumRadius = new UnitValueJson(0, DistanceUnit.INCHES.toString()); + public UnitValueJson lowerSoftLimit = new UnitValueJson(0, DistanceUnit.METERS.toString()); + public UnitValueJson upperSoftLimit = new UnitValueJson(0, DistanceUnit.METERS.toString()); + public UnitValueJson lowerHardLimit = new UnitValueJson(0, DistanceUnit.METERS.toString()); + public UnitValueJson upperHardLimit = new UnitValueJson(0, DistanceUnit.METERS.toString()); public double[] gearing; - public UnitValueJson startingPosition = new UnitValueJson(0, UnitsParser.M); - public UnitValueJson mass = new UnitValueJson(0, UnitsParser.LBS); - public UnitValueJson voltageCompensation = new UnitValueJson(12, UnitsParser.VOLTS); + public String gearStages = ""; + public UnitValueJson startingPosition = new UnitValueJson(0, DistanceUnit.METERS.toString()); + public UnitValueJson mass = new UnitValueJson(0, MassUnit.POUNDS.toString()); + public UnitValueJson voltageCompensation = new UnitValueJson(12, VoltageUnit.VOLTS.toString()); /** * Configure the given GenericSubsystem with an elevator using the given json configuration. @@ -41,9 +44,6 @@ public class YamsElevatorConfigurationJson implements DeviceConfiguration { */ @Override public Elevator configure(SubsystemBase deviceHandler) { - GenericMotorController motor = - DeviceConfigReader.getMotor( - motorSetup.controllerType, motorSetup.motorType, motorSetup.canId); SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(deviceHandler); if (sprocketTeeth > 0) { motorConfig.withMechanismCircumference(Meters.of(Inches.of(0.25).in(Meters) * sprocketTeeth)); @@ -54,40 +54,45 @@ public Elevator configure(SubsystemBase deviceHandler) { } motorConfig - .withClosedLoopController( - motorSystemId.feedBack.p, - motorSystemId.feedBack.i, - motorSystemId.feedBack.d, - UnitsParser.parseVelocity(motorSystemId.maxVelocity), - UnitsParser.parseAccelleration(motorSystemId.maxAcceleration)) .withSoftLimit( UnitsParser.parseDistance(lowerSoftLimit), UnitsParser.parseDistance(upperSoftLimit)) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(gearing))) - .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) - .withTelemetry(motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) - .withMotorInverted(motorSetup.inverted) - .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)) - .withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)) .withFeedforward( new ElevatorFeedforward( motorSystemId.feedForward.s, motorSystemId.feedForward.g, motorSystemId.feedForward.v, motorSystemId.feedForward.a)) - .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); - MotorSetupJson.setupFollowers(motorConfig, motorSetup); - motor.setMotorSimulationType( - motor.getMotorConfig().getMotorSimulationType(motorSetup.numberOfMotors)); + .withSimFeedforward( + new ElevatorFeedforward( + simSystemId.feedForward.s, + simSystemId.feedForward.g, + simSystemId.feedForward.v, + simSystemId.feedForward.a)); + + YamsConfigCommon.PhysicalParameters physicalParams = + new YamsConfigCommon.PhysicalParameters( + voltageCompensation, mass, drumRadius, gearing, gearStages); - SmartMotorController smartMotor = motor.getSmartMotorController(motorConfig); + Optional smartMotor = + YamsConfigCommon.configureSmartMotorController( + motorSetup, motorConfig, controlAlgorithm, motorSystemId, simSystemId, physicalParams); + if (smartMotor.isEmpty()) { + throw new RuntimeException( + "Elevator Smart motor configuration issue. ID: " + + motorSetup.canId + + " Controller: " + + motorSetup.controllerType + + " Motor: " + + motorSetup.motorType); + } ElevatorConfig m_config = - new ElevatorConfig(smartMotor) + new ElevatorConfig(smartMotor.get()) .withStartingHeight(UnitsParser.parseDistance(startingPosition)) .withHardLimits( UnitsParser.parseDistance(lowerHardLimit), UnitsParser.parseDistance(upperHardLimit)) .withTelemetry(motorSetup.name, TelemetryVerbosity.valueOf(motorSetup.logLevel)) + .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) .withMass(UnitsParser.parseMass(mass)); Elevator elevator = new Elevator(m_config); return elevator; diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsPivotConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsPivotConfigurationJson.java index 92c5a0dd..e5beed9e 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsPivotConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsPivotConfigurationJson.java @@ -6,33 +6,39 @@ import edu.wpi.first.math.controller.ArmFeedforward; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import java.util.Optional; +import org.frc5010.common.config.ConfigConstants.ControlAlgorithm; import org.frc5010.common.config.DeviceConfiguration; import org.frc5010.common.config.UnitsParser; import org.frc5010.common.config.json.UnitValueJson; -import org.frc5010.common.motors.GenericMotorController; -import yams.gearing.GearBox; -import yams.gearing.MechanismGearing; +import org.frc5010.common.config.units.AngleUnit; +import org.frc5010.common.config.units.DistanceUnit; +import org.frc5010.common.config.units.MassUnit; +import org.frc5010.common.config.units.VoltageUnit; import yams.mechanisms.config.PivotConfig; import yams.mechanisms.positional.Pivot; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; /** Configuration for a YamsTurret */ public class YamsPivotConfigurationJson implements DeviceConfiguration { public MotorSetupJson motorSetup = new MotorSetupJson(); + public ControlAlgorithm controlAlgorithm = ControlAlgorithm.SIMPLE; public MotorSystemIdJson motorSystemId = new MotorSystemIdJson(); - public UnitValueJson lowerHardLimit = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson upperHardLimit = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson startingAngle = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson lowerSoftLimit = new UnitValueJson(0, UnitsParser.DEG); - public UnitValueJson upperSoftLimit = new UnitValueJson(0, UnitsParser.DEG); + public MotorSystemIdJson simSystemId = new MotorSystemIdJson(); + public UnitValueJson lowerHardLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson upperHardLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson startingAngle = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson lowerSoftLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); + public UnitValueJson upperSoftLimit = new UnitValueJson(0, AngleUnit.DEGREES.toString()); public double[] gearing; - public UnitValueJson voltageCompensation = new UnitValueJson(12, UnitsParser.VOLTS); - public UnitValueJson startingPosition = new UnitValueJson(0, UnitsParser.DEG); - public double moi; + public String gearStages = ""; + public UnitValueJson voltageCompensation = new UnitValueJson(12, VoltageUnit.VOLTS.toString()); + public UnitValueJson radius = new UnitValueJson(1, DistanceUnit.INCHES.toString()); + public UnitValueJson mass = new UnitValueJson(1, MassUnit.POUNDS.toString()); + public UnitValueJson startingPosition = new UnitValueJson(0, DistanceUnit.METERS.toString()); /** * Configure the given GenericSubsystem with a pivot using the given json configuration. @@ -42,47 +48,48 @@ public class YamsPivotConfigurationJson implements DeviceConfiguration { */ @Override public Pivot configure(SubsystemBase deviceHandler) { - GenericMotorController motor = - DeviceConfigReader.getMotor( - motorSetup.controllerType, motorSetup.motorType, motorSetup.canId); - SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(deviceHandler) - .withClosedLoopController( - motorSystemId.feedBack.p, - motorSystemId.feedBack.i, - motorSystemId.feedBack.d, - UnitsParser.parseAngularVelocity(motorSystemId.maxVelocity), - UnitsParser.parseAngularAcceleration(motorSystemId.maxAcceleration)) .withSoftLimit( UnitsParser.parseAngle(lowerSoftLimit), UnitsParser.parseAngle(upperSoftLimit)) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(gearing))) - .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) - .withTelemetry( - motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) - .withMotorInverted(motorSetup.inverted) - .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)) - .withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)) .withFeedforward( new ArmFeedforward( motorSystemId.feedForward.s, - motorSystemId.feedForward.g, + 0, motorSystemId.feedForward.v, motorSystemId.feedForward.a)) + .withSimFeedforward( + new ArmFeedforward( + simSystemId.feedForward.s, + 0, + simSystemId.feedForward.v, + simSystemId.feedForward.a)) .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); - MotorSetupJson.setupFollowers(motorConfig, motorSetup); - motor.setMotorSimulationType( - motor.getMotorConfig().getMotorSimulationType(motorSetup.numberOfMotors)); - SmartMotorController smartMotor = motor.getSmartMotorController(motorConfig); + YamsConfigCommon.PhysicalParameters physicalParams = + new YamsConfigCommon.PhysicalParameters( + voltageCompensation, mass, radius, gearing, gearStages); + + Optional smartMotor = + YamsConfigCommon.configureSmartMotorController( + motorSetup, motorConfig, controlAlgorithm, motorSystemId, simSystemId, physicalParams); + if (smartMotor.isEmpty()) { + throw new RuntimeException( + "Smart motor configuration issue. ID: " + + motorSetup.canId + + " Controller: " + + motorSetup.controllerType + + " Motor: " + + motorSetup.motorType); + } PivotConfig pivotConfig = - new PivotConfig(smartMotor) + new PivotConfig(smartMotor.get()) .withHardLimit( UnitsParser.parseAngle(lowerHardLimit), UnitsParser.parseAngle(upperHardLimit)) .withTelemetry(motorSetup.name, TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withStartingPosition(UnitsParser.parseAngle(startingPosition)) - .withMOI(moi); + .withStartingPosition(UnitsParser.parseAngle(startingAngle)) + .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) + .withMOI(UnitsParser.parseDistance(radius), UnitsParser.parseMass(mass)); Pivot pivot = new Pivot(pivotConfig); return pivot; } diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java index dcd8bacd..95fbf755 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsShooterConfigurationJson.java @@ -6,33 +6,37 @@ import edu.wpi.first.math.controller.SimpleMotorFeedforward; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import java.util.Optional; +import org.frc5010.common.config.ConfigConstants.ControlAlgorithm; import org.frc5010.common.config.DeviceConfiguration; import org.frc5010.common.config.UnitsParser; import org.frc5010.common.config.json.UnitValueJson; -import org.frc5010.common.motors.GenericMotorController; -import yams.gearing.GearBox; -import yams.gearing.MechanismGearing; +import org.frc5010.common.config.units.AngularVelocityUnit; +import org.frc5010.common.config.units.DistanceUnit; +import org.frc5010.common.config.units.MassUnit; +import org.frc5010.common.config.units.VoltageUnit; import yams.mechanisms.config.FlyWheelConfig; import yams.mechanisms.velocity.FlyWheel; import yams.motorcontrollers.SmartMotorController; import yams.motorcontrollers.SmartMotorControllerConfig; import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; /** Add your docs here. */ public class YamsShooterConfigurationJson implements DeviceConfiguration { public MotorSetupJson motorSetup = new MotorSetupJson(); + public ControlAlgorithm controlAlgorithm = ControlAlgorithm.SIMPLE; public MotorSystemIdJson motorSystemId = new MotorSystemIdJson(); - public MotorSystemIdJson simMotorSystemId = new MotorSystemIdJson(); - public UnitValueJson lowerSoftLimit = new UnitValueJson(0, UnitsParser.DEGPS); - public UnitValueJson upperSoftLimit = new UnitValueJson(0, UnitsParser.DEGPS); - public double[] gearing = new double[0]; - public String stages = ""; - public UnitValueJson voltageCompensation = new UnitValueJson(12, UnitsParser.VOLTS); - public UnitValueJson mass = new UnitValueJson(0, UnitsParser.LBS); - public UnitValueJson diameter = new UnitValueJson(0, UnitsParser.IN); - public double moi = 0; + public MotorSystemIdJson simSystemId = new MotorSystemIdJson(); + public UnitValueJson lowerSoftLimit = + new UnitValueJson(0, AngularVelocityUnit.DEGREES_PER_SECOND.toString()); + public UnitValueJson upperSoftLimit = + new UnitValueJson(0, AngularVelocityUnit.DEGREES_PER_SECOND.toString()); + public double[] gearing; + public String gearStages = ""; + public UnitValueJson voltageCompensation = new UnitValueJson(12, VoltageUnit.VOLTS.toString()); + public UnitValueJson mass = new UnitValueJson(0, MassUnit.POUNDS.toString()); + public UnitValueJson radius = new UnitValueJson(0, DistanceUnit.INCHES.toString()); /** * Configure the given GenericSubsystem with a shooter using the given json configuration. @@ -42,73 +46,45 @@ public class YamsShooterConfigurationJson implements DeviceConfiguration { */ @Override public FlyWheel configure(SubsystemBase deviceHandler) { - GenericMotorController motor = - DeviceConfigReader.getMotor( - motorSetup.controllerType, motorSetup.motorType, motorSetup.canId); + SmartMotorControllerConfig motorConfig = + new SmartMotorControllerConfig(deviceHandler) + .withFeedforward( + new SimpleMotorFeedforward( + motorSystemId.feedForward.s, + motorSystemId.feedForward.v, + motorSystemId.feedForward.a)) + .withSimFeedforward( + new SimpleMotorFeedforward( + simSystemId.feedForward.s, + simSystemId.feedForward.v, + simSystemId.feedForward.a)) + .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); + YamsConfigCommon.PhysicalParameters physicalParams = + new YamsConfigCommon.PhysicalParameters( + voltageCompensation, mass, radius, gearing, gearStages); - SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(deviceHandler); - if (motorSystemId.maxAcceleration.val > 0) { - motorConfig.withClosedLoopController( - motorSystemId.feedBack.p, - motorSystemId.feedBack.i, - motorSystemId.feedBack.d, - UnitsParser.parseAngularVelocity(motorSystemId.maxVelocity), - UnitsParser.parseAngularAcceleration(motorSystemId.maxAcceleration)); - } else { - motorConfig.withClosedLoopController( - motorSystemId.feedBack.p, motorSystemId.feedBack.i, motorSystemId.feedBack.d); + Optional smartMotor = + YamsConfigCommon.configureSmartMotorController( + motorSetup, motorConfig, controlAlgorithm, motorSystemId, simSystemId, physicalParams); + if (smartMotor.isEmpty()) { + throw new RuntimeException( + "Smart motor not found. ID: " + + motorSetup.canId + + " Controller: " + + motorSetup.controllerType + + " Motor: " + + motorSetup.motorType); } - if (simMotorSystemId.maxAcceleration.val > 0) { - motorConfig.withSimClosedLoopController( - simMotorSystemId.feedBack.p, - simMotorSystemId.feedBack.i, - simMotorSystemId.feedBack.d, - UnitsParser.parseAngularVelocity(simMotorSystemId.maxVelocity), - UnitsParser.parseAngularAcceleration(simMotorSystemId.maxAcceleration)); - } else { - motorConfig.withSimClosedLoopController( - simMotorSystemId.feedBack.p, simMotorSystemId.feedBack.i, simMotorSystemId.feedBack.d); - } - if (gearing.length > 0) { - motorConfig.withGearing(new MechanismGearing(GearBox.fromReductionStages(gearing))); - } - if (!stages.isBlank()) { - motorConfig.withGearing(new MechanismGearing(GearBox.fromStages(stages))); - } - motorConfig - .withIdleMode(MotorMode.valueOf(motorSetup.idleMode)) - .withTelemetry(motorSetup.name + "Motor", TelemetryVerbosity.valueOf(motorSetup.logLevel)) - .withStatorCurrentLimit(UnitsParser.parseAmps(motorSetup.currentLimit)) - .withMotorInverted(motorSetup.inverted) - .withClosedLoopRampRate(UnitsParser.parseTime(motorSystemId.closedLoopRamp)); - if (!motorSetup.controllerType.equals("thrifty")) { - motorConfig.withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)); - } - motorConfig - .withFeedforward( - new SimpleMotorFeedforward( - motorSystemId.feedForward.s, - motorSystemId.feedForward.v, - motorSystemId.feedForward.a)) - .withControlMode(ControlMode.valueOf(motorSystemId.controlMode)); - MotorSetupJson.setupFollowers(motorConfig, motorSetup); - motor.setMotorSimulationType( - motor.getMotorConfig().getMotorSimulationType(motorSetup.numberOfMotors)); - - SmartMotorController smartMotor = motor.getSmartMotorController(motorConfig); FlyWheelConfig shooterConfig = - new FlyWheelConfig(smartMotor) - // .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) - .withDiameter(UnitsParser.parseDistance(diameter)) + new FlyWheelConfig(smartMotor.get()) + .withMechanismPositionConfig(motorSetup.getMechanismPositionConfig()) + .withDiameter(UnitsParser.parseDistance(radius).times(2.0)) .withMass(UnitsParser.parseMass(mass)) - .withSoftLimit( - UnitsParser.parseAngularVelocity(lowerSoftLimit), - UnitsParser.parseAngularVelocity(upperSoftLimit)) + .withUpperSoftLimit(UnitsParser.parseAngularVelocity(upperSoftLimit)) + .withLowerSoftLimit(UnitsParser.parseAngularVelocity(lowerSoftLimit)) .withSpeedometerSimulation(UnitsParser.parseAngularVelocity(upperSoftLimit)) .withTelemetry(motorSetup.name, TelemetryVerbosity.valueOf(motorSetup.logLevel)); - if (0 != moi) { - shooterConfig.withMOI(moi); - } + shooterConfig.withMOI(UnitsParser.parseDistance(radius), UnitsParser.parseMass(mass)); FlyWheel shooter = new FlyWheel(shooterConfig); return shooter; } diff --git a/src/main/java/org/frc5010/common/config/units/AngleUnit.java b/src/main/java/org/frc5010/common/config/units/AngleUnit.java new file mode 100644 index 00000000..bca1faf6 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/AngleUnit.java @@ -0,0 +1,43 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing angle unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum AngleUnit { + DEGREES("deg", "degrees"), + RADIANS("rad", "radians"), + ROTATIONS("rot", "rotation", "rotations"); + + private final String[] aliases; + + AngleUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into an AngleUnit. + * + * @param unitString The string representation of the unit + * @return The matching AngleUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static AngleUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (AngleUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown angle unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/AngularAccelerationUnit.java b/src/main/java/org/frc5010/common/config/units/AngularAccelerationUnit.java new file mode 100644 index 00000000..d44481b2 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/AngularAccelerationUnit.java @@ -0,0 +1,54 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing angular acceleration unit types. Each enum value can be parsed from multiple + * string representations. + */ +public enum AngularAccelerationUnit { + DEGREES_PER_SECOND_SQUARED( + "deg/s^2", "deg/s/s", "deg/s2", "degrees/s/s", "degrees/s2", "degrees/s^2"), + RADIANS_PER_SECOND_SQUARED( + "rad/s^2", + "rads/s/s", + "rads/s2", + "rads/s^2", + "radians", + "radians/s/s", + "radians/s2", + "radians/s^2"), + REVOLUTIONS_PER_MINUTE_PER_SECOND("rpm/s", "RPM/s", "rpm/sec", "RPM/sec"), + ROTATIONS_PER_SECOND_SQUARED( + "rps^2", "RPS^2", "RPS2", "RPS/s", "RPS/sec", "rps2", "rps/s", "rps/sec"); + + private final String[] aliases; + + AngularAccelerationUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into an AngularAccelerationUnit. + * + * @param unitString The string representation of the unit + * @return The matching AngularAccelerationUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static AngularAccelerationUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (AngularAccelerationUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown angular acceleration unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/AngularVelocityUnit.java b/src/main/java/org/frc5010/common/config/units/AngularVelocityUnit.java new file mode 100644 index 00000000..70d0dc50 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/AngularVelocityUnit.java @@ -0,0 +1,55 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing angular velocity unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum AngularVelocityUnit { + DEGREES_PER_SECOND( + "deg/s", "deg/sec", "deg/second", "degrees/s", "degrees/sec", "degrees/second"), + RADIANS_PER_SECOND( + "rad/s", + "rad/sec", + "rad/second", + "rads/s", + "rads/sec", + "rads/second", + "radians", + "radians/s", + "radians/sec", + "radians/second"), + REVOLUTIONS_PER_MINUTE("rpm", "RPM"), + ROTATIONS_PER_SECOND("rps", "RPS"); + + private final String[] aliases; + + AngularVelocityUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into an AngularVelocityUnit. + * + * @param unitString The string representation of the unit + * @return The matching AngularVelocityUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static AngularVelocityUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (AngularVelocityUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown angular velocity unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/CurrentUnit.java b/src/main/java/org/frc5010/common/config/units/CurrentUnit.java new file mode 100644 index 00000000..b328c86c --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/CurrentUnit.java @@ -0,0 +1,43 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing current unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum CurrentUnit { + AMPS("a", "amp", "amps", "ampere", "amperes"), + MILLIAMPS("ma", "milliamp", "milliamps", "milliampere", "milliamperes"), + MICROAMPS("ua", "microamp", "microamps", "microampere", "microamperes"); + + private final String[] aliases; + + CurrentUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a CurrentUnit. + * + * @param unitString The string representation of the unit + * @return The matching CurrentUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static CurrentUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (CurrentUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown current unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/DistanceUnit.java b/src/main/java/org/frc5010/common/config/units/DistanceUnit.java new file mode 100644 index 00000000..d8bc2960 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/DistanceUnit.java @@ -0,0 +1,46 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing distance unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum DistanceUnit { + METERS("m", "meter", "meters"), + INCHES("in", "inch", "inches"), + FEET("ft", "foot", "feet"), + MILLIMETERS("mm", "millimeter", "millimeters"), + CENTIMETERS("cm", "centimeter", "centimeters"), + YARDS("yd", "yard", "yards"); + + private final String[] aliases; + + DistanceUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a DistanceUnit. + * + * @param unitString The string representation of the unit + * @return The matching DistanceUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static DistanceUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (DistanceUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown distance unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/LinearAccelerationUnit.java b/src/main/java/org/frc5010/common/config/units/LinearAccelerationUnit.java new file mode 100644 index 00000000..a1656b0d --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/LinearAccelerationUnit.java @@ -0,0 +1,108 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing linear acceleration unit types. Each enum value can be parsed from multiple + * string representations. + */ +public enum LinearAccelerationUnit { + METERS_PER_SECOND_SQUARED( + "m/s^2", + "m/s/s", + "m/s2", + "m/sec/sec", + "m/sec2", + "m/sec^2", + "meters/sec/sec", + "meters/sec2", + "meters/sec^2", + "meters/second/second", + "meters/second2", + "meters/second^2"), + INCHES_PER_SECOND_SQUARED( + "in/s^2", + "in/s/s", + "in/s2", + "in/sec/sec", + "in/sec2", + "in/sec^2", + "inches/sec/sec", + "inches/sec2", + "inches/sec^2", + "inches/second/second", + "inches/second2", + "inches/second^2"), + FEET_PER_SECOND_SQUARED( + "ft/s^2", + "ft/s/s", + "ft/s2", + "ft/sec/sec", + "ft/sec2", + "ft/sec^2", + "feet/sec/sec", + "feet/sec2", + "feet/sec^2", + "feet/second/second", + "feet/second2", + "feet/second^2"), + MILLIMETERS_PER_SECOND_SQUARED( + "mm/s^2", + "mm/s/s", + "mm/s2", + "millimeter/sec/sec", + "millimeter/sec2", + "millimeter/sec^2", + "millimeters/sec/sec", + "millimeters/sec2", + "millimeters/sec^2", + "millimeters/second/second", + "millimeters/second2", + "millimeters/second^2"), + CENTIMETERS_PER_SECOND_SQUARED( + "cm/s^2", + "cm/sec^2", + "cm/second^2", + "centimeter/sec^2", + "centimeter/second^2", + "centimeters/sec^2", + "centimeters/second^2", + "cms/s^2", + "cms/sec^2", + "cms/second^2", + "cm/s/s", + "cm/sec/sec", + "cm/second/second", + "centimeters/sec/sec", + "centimeters/second/second"); + + private final String[] aliases; + + LinearAccelerationUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a LinearAccelerationUnit. + * + * @param unitString The string representation of the unit + * @return The matching LinearAccelerationUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static LinearAccelerationUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (LinearAccelerationUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown linear acceleration unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/LinearVelocityUnit.java b/src/main/java/org/frc5010/common/config/units/LinearVelocityUnit.java new file mode 100644 index 00000000..066a177b --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/LinearVelocityUnit.java @@ -0,0 +1,58 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing linear velocity unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum LinearVelocityUnit { + METERS_PER_SECOND( + "m/s", "m/sec", "mps", "meter/sec", "meters/sec", "meter/second", "meters/second"), + INCHES_PER_SECOND("in/s", "in/sec", "inch/sec", "inches/sec", "inch/second", "inches/second"), + FEET_PER_SECOND("ft/s", "ft/sec", "foot/sec", "feet/sec", "foot/second", "feet/second"), + MILLIMETERS_PER_SECOND( + "mm/s", + "mm/sec", + "millimeter/sec", + "millimeters/sec", + "millimeter/second", + "millimeters/second"), + CENTIMETERS_PER_SECOND( + "cm/s", + "cm/sec", + "centimeter/sec", + "centimeters/sec", + "centimeter/second", + "centimeters/second"); + + private final String[] aliases; + + LinearVelocityUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a LinearVelocityUnit. + * + * @param unitString The string representation of the unit + * @return The matching LinearVelocityUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static LinearVelocityUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (LinearVelocityUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown linear velocity unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/MassUnit.java b/src/main/java/org/frc5010/common/config/units/MassUnit.java new file mode 100644 index 00000000..a8ab8e3f --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/MassUnit.java @@ -0,0 +1,47 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing mass unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum MassUnit { + KILOGRAMS("kg", "kgs", "kilogram", "kilograms"), + GRAMS("g", "gram", "grams"), + MILLIGRAMS("mg", "milligram", "milligrams"), + OUNCES("oz", "ounce", "ounces"), + POUNDS("lbs", "lb", "pound", "pounds"), + STONE("st", "stone", "stones"), + TONS("t", "tons", "ton"); + + private final String[] aliases; + + MassUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a MassUnit. + * + * @param unitString The string representation of the unit + * @return The matching MassUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static MassUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (MassUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown mass unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/MomentOfInertiaUnit.java b/src/main/java/org/frc5010/common/config/units/MomentOfInertiaUnit.java new file mode 100644 index 00000000..97aeed48 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/MomentOfInertiaUnit.java @@ -0,0 +1,41 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing moment of inertia unit types. Each enum value can be parsed from multiple + * string representations. + */ +public enum MomentOfInertiaUnit { + KILOGRAM_SQUARE_METERS("kg*m^2", "kg*m2", "kg*sqm"); + + private final String[] aliases; + + MomentOfInertiaUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a MomentOfInertiaUnit. + * + * @param unitString The string representation of the unit + * @return The matching MomentOfInertiaUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static MomentOfInertiaUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (MomentOfInertiaUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown moment of inertia unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/TimeUnit.java b/src/main/java/org/frc5010/common/config/units/TimeUnit.java new file mode 100644 index 00000000..3dd2128d --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/TimeUnit.java @@ -0,0 +1,47 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing time unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum TimeUnit { + SECONDS("s", "sec", "second", "seconds"), + MILLISECONDS("ms", "millisecond", "milliseconds"), + MICROSECONDS("us", "microsecond", "microseconds"), + NANOSECONDS("ns", "nanosecond", "nanoseconds"), + MINUTES("min", "minute", "minutes"), + HOURS("h", "hour", "hours"), + DAYS("d", "day", "days"); + + private final String[] aliases; + + TimeUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a TimeUnit. + * + * @param unitString The string representation of the unit + * @return The matching TimeUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static TimeUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (TimeUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown time unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/config/units/VoltageUnit.java b/src/main/java/org/frc5010/common/config/units/VoltageUnit.java new file mode 100644 index 00000000..14dc7ea4 --- /dev/null +++ b/src/main/java/org/frc5010/common/config/units/VoltageUnit.java @@ -0,0 +1,44 @@ +package org.frc5010.common.config.units; + +/** + * Enum representing voltage unit types. Each enum value can be parsed from multiple string + * representations. + */ +public enum VoltageUnit { + VOLTS("v", "volt", "volts", "voltage"), + MILLIVOLTS("mv", "millivolt", "millivolts"), + MICROVOLTS("uv", "microvolt", "microvolts"), + KILOVOLTS("kv", "kilovolt", "kilovolts"); + + private final String[] aliases; + + VoltageUnit(String... aliases) { + this.aliases = aliases; + } + + /** + * Attempts to parse a string into a VoltageUnit. + * + * @param unitString The string representation of the unit + * @return The matching VoltageUnit + * @throws IllegalArgumentException if no matching unit is found + */ + public static VoltageUnit fromString(String unitString) { + if (unitString == null) { + throw new IllegalArgumentException("Unit string cannot be null"); + } + String normalized = unitString.trim().toLowerCase(); + for (VoltageUnit unit : values()) { + for (String alias : unit.aliases) { + if (alias.equals(normalized)) { + return unit; + } + } + } + throw new IllegalArgumentException("Unknown voltage unit: " + unitString); + } + + public String[] getAliases() { + return aliases; + } +} diff --git a/src/main/java/org/frc5010/common/constants/Constants.java b/src/main/java/org/frc5010/common/constants/Constants.java index bcacb47d..b516a51c 100644 --- a/src/main/java/org/frc5010/common/constants/Constants.java +++ b/src/main/java/org/frc5010/common/constants/Constants.java @@ -5,6 +5,8 @@ /** A class for library constants */ public class Constants { + public static final double loopPeriodSecs = 0.02; + /** Auton constants */ public static final class AutonConstants { /** Translation PID constants */ diff --git a/src/main/java/org/frc5010/common/drive/DifferentialDrivetrain.java b/src/main/java/org/frc5010/common/drive/DifferentialDrivetrain.java index 0223d3b8..4d77ea91 100644 --- a/src/main/java/org/frc5010/common/drive/DifferentialDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/DifferentialDrivetrain.java @@ -208,7 +208,7 @@ public void setAutoBuilder() { this::drive, new PPLTVController(0.02), // PPLTVController is the built in path following controller for differential drive trains - config, // The robot configuration + ppRobotConfig, // The robot configuration () -> { // Boolean supplier that controls when the path will be mirrored for the red // alliance diff --git a/src/main/java/org/frc5010/common/drive/DrivetrainConfig.java b/src/main/java/org/frc5010/common/drive/DrivetrainConfig.java new file mode 100644 index 00000000..5518d7b4 --- /dev/null +++ b/src/main/java/org/frc5010/common/drive/DrivetrainConfig.java @@ -0,0 +1,487 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.drive; + +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.LinearVelocity; +import edu.wpi.first.units.measure.Mass; +import edu.wpi.first.units.measure.MomentOfInertia; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import org.frc5010.common.config.UnitsParser; +import org.frc5010.common.config.json.devices.DrivetrainConstantsJson; +import org.frc5010.common.sensors.gyro.GenericGyro; +import yams.gearing.GearBox; +import yams.gearing.MechanismGearing; +import yams.motorcontrollers.SmartMotorControllerConfig; + +/** A class containing constants for the drivetrain */ +public class DrivetrainConfig { + /** The track width */ + protected Distance trackWidth; + /** The wheel base */ + protected Distance wheelBase; + /** The width of the drivetrain including the bumpers */ + protected Distance bumperFrameWidth; + /** The length of the drivetrain including the bumpers */ + protected Distance bumperFrameLength; + /** The wheel diameter */ + protected Distance wheelDiameter; + /** The maximum drive speed */ + protected LinearVelocity maxDriveSpeed; + /** The drive gear ratio */ + protected double driveGearRatio; + /** The current draw at which the wheels slip */ + protected Current slipCurrent; + /** Whether to invert the left side */ + protected boolean invertLeftSide; + /** Whether to invert the right side */ + protected boolean invertRightSide; + /** The drive inertia */ + protected MomentOfInertia driveInertia; + /** The robot mass */ + protected Mass robotMass; + /** The drivetrain type */ + protected String drivetrainType; + /** The gyro used by the drivetrain */ + protected GenericGyro gyro; + /** The drive motor controller configuration */ + protected SmartMotorControllerConfig driveMotorControlConfig; + /** The CAN bus for the drivetrain */ + protected String canbus; + + /** + * Returns a new drivetrain configuration builder. + * + * @return a new drivetrain configuration builder + */ + public static DrivetrainConfigBuilder builder() { + return new DrivetrainConfigBuilder(); + } + + protected DrivetrainConfig() {} + + protected DrivetrainConfig(DrivetrainConfigBuilder builder) { + this.trackWidth = builder.trackWidth; + this.wheelDiameter = builder.wheelDiameter; + this.maxDriveSpeed = builder.maxDriveSpeed; + this.driveGearRatio = builder.driveGearRatio; + this.slipCurrent = builder.slipCurrent; + this.invertLeftSide = builder.invertLeftSide; + this.invertRightSide = builder.invertRightSide; + this.driveInertia = builder.driveInertia; + this.robotMass = builder.robotMass; + this.drivetrainType = builder.drivetrainType; + this.gyro = builder.gyro; + this.driveMotorControlConfig = builder.driveMotorControlConfig; + this.canbus = builder.canbus; + this.wheelBase = builder.wheelBase; + this.bumperFrameWidth = builder.bumperFrameWidth; + this.bumperFrameLength = builder.bumperFrameLength; + } + + /** + * Returns the track width of the drivetrain, which is the distance between the center of the left + * and right wheels. + * + * @return the track width of the drivetrain, in meters. + */ + public Distance getTrackWidth() { + return trackWidth; + } + + /** + * Returns the wheel base of the drivetrain. + * + * @return The wheel base of the drivetrain, in meters. + */ + public Distance getWheelBase() { + return wheelBase; + } + + /** + * Returns the frame width of the drivetrain including the bumpers. + * + * @return The frame width of the drivetrain including the bumpers + */ + public Distance getBumperFrameWidth() { + return bumperFrameWidth; + } + + /** + * Returns the frame length of the drivetrain including the bumpers. + * + * @return The frame length of the drivetrain including the bumpers + */ + public Distance getBumperFrameLength() { + return bumperFrameLength; + } + + /** + * Returns the diameter of the wheels on the drivetrain. + * + * @return The diameter of the wheels on the drivetrain, in meters. + */ + public Distance getWheelDiameter() { + return wheelDiameter; + } + + /** + * Returns the maximum speed the drivetrain can travel at. + * + * @return the maximum speed the drivetrain can travel at, in meters per second + */ + public LinearVelocity getMaxDriveSpeed() { + return maxDriveSpeed; + } + + /** + * Gets the gear ratio of the drive motor. + * + * @return the gear ratio of the drive motor + */ + public double getDriveGearRatio() { + return driveGearRatio; + } + + /** + * Gets the current at which the wheels slip. + * + * @return the current at which the wheels slip, in Amps + */ + public Current getSlipCurrent() { + return slipCurrent; + } + + /** + * Returns whether the left side of the drivetrain should be inverted. + * + * @return true if the left side should be inverted, false otherwise + */ + public boolean isInvertLeftSide() { + return invertLeftSide; + } + + /** + * Gets whether the right side of the drivetrain should be inverted. + * + * @return true if the right side should be inverted, false otherwise + */ + public boolean isInvertRightSide() { + return invertRightSide; + } + + /** + * Gets the moment of inertia of the drivetrain. + * + * @return the moment of inertia of the drivetrain in kg*m^2 + */ + public MomentOfInertia getDriveInertia() { + return driveInertia; + } + + /** + * Gets the robot's mass. + * + * @return the robot's mass in kg + */ + public Mass getRobotMass() { + return robotMass; + } + + /** + * Returns the type of the drivetrain. + * + * @return The type of the drivetrain + */ + public String getDrivetrainType() { + return drivetrainType; + } + + /** + * Returns the configuration for the drive motor controller. + * + * @return the configuration for the drive motor controller + */ + public SmartMotorControllerConfig getDriveMotorControlConfig() { + return driveMotorControlConfig; + } + + /** + * Gets the gyro associated with this drivetrain configuration. + * + * @return the gyro associated with this drivetrain configuration + */ + public GenericGyro getGyro() { + return gyro; + } + + /** + * Gets the canbus string associated with this drivetrain configuration. + * + * @return the canbus string associated with this drivetrain configuration + */ + public String getCanBusName() { + return canbus; + } + + // *********************************************** + + /** A builder for drivetrain configurations */ + public static class DrivetrainConfigBuilder { + protected Distance trackWidth; + protected Distance wheelBase; + protected Distance wheelDiameter; + protected Distance bumperFrameWidth; + protected Distance bumperFrameLength; + protected LinearVelocity maxDriveSpeed; + protected double driveGearRatio; + protected Current slipCurrent; + protected boolean invertLeftSide; + protected boolean invertRightSide; + protected MomentOfInertia driveInertia; + protected Mass robotMass; + protected String drivetrainType; + protected GenericGyro gyro; + protected SmartMotorControllerConfig driveMotorControlConfig; + protected String canbus; + + /** Creates a new drivetrain configuration builder. */ + protected DrivetrainConfigBuilder() {} + + /** + * Builds a new drivetrain configuration from the values set in this builder. + * + * @return a new drivetrain configuration + */ + public DrivetrainConfig build(DrivetrainConstantsJson constants, SubsystemBase subsystem) { + withDriveGearRatio( + new MechanismGearing(GearBox.fromStages(constants.driveGearRatio)) + .getRotorToMechanismRatio()); + withInvertLeftSide(constants.invertLeftSide); + withInvertRightSide(constants.invertRightSide); + withDriveInertia(UnitsParser.parseMomentOfInertia(constants.driveInertia)); + withMaxDriveSpeed(UnitsParser.parseVelocity(constants.maxDriveSpeed)); + withRobotMass(UnitsParser.parseMass(constants.robotMass)); + withTrackWidth(UnitsParser.parseDistance(constants.trackWidth)); + withWheelBase(UnitsParser.parseDistance(constants.wheelBase)); + withBumperFrameLength(UnitsParser.parseDistance(constants.bumperFrameLength)); + withBumperFrameWidth(UnitsParser.parseDistance(constants.bumperFrameWidth)); + withWheelDiameter(UnitsParser.parseDistance(constants.wheelDiameter)); + withSlipCurrent(UnitsParser.parseAmps(constants.slipCurrent)); + withGyro((GenericGyro) constants.gyro.configure(subsystem)); + withCanbus(constants.canbus); + + return new DrivetrainConfig(this); + } + + /** + * Set the track width of the drivetrain. + * + *

This value should be set to the distance between the center of the left and right wheels + * on the drivetrain in meters. This value is used by the drivetrain to calculate its linear + * velocity. + * + * @param trackWidth the track width of the drivetrain in meters + * @return this builder + */ + public DrivetrainConfigBuilder withTrackWidth(Distance trackWidth) { + this.trackWidth = trackWidth; + return this; + } + + /** + * Set the wheel base of the drivetrain. + * + *

This value should be set to the distance between the centers of the front and back wheels + * on the drivetrain in meters. This value is used by the drivetrain to calculate its linear + * velocity. + * + * @param wheelBase the wheel base of the drivetrain in meters + * @return this builder + */ + public DrivetrainConfigBuilder withWheelBase(Distance wheelBase) { + this.wheelBase = wheelBase; + return this; + } + + /** + * Set the diameter of the wheels on the drivetrain. + * + *

This value should be set to the diameter of the wheels on the drivetrain in meters. This + * value is used by the drivetrain to calculate its linear velocity. + * + * @param wheelDiameter the diameter of the wheels on the drivetrain in meters + * @return this builder + */ + public DrivetrainConfigBuilder withWheelDiameter(Distance wheelDiameter) { + this.wheelDiameter = wheelDiameter; + return this; + } + + /** + * Set the maximum speed of the drivetrain. + * + *

This value should be set to the maximum speed that the drivetrain can reach. This value is + * used to limit the speed of the drivetrain and prevent it from running too quickly. + * + * @param maxDriveSpeed the maximum speed of the drivetrain + * @return this builder + */ + public DrivetrainConfigBuilder withMaxDriveSpeed(LinearVelocity maxDriveSpeed) { + this.maxDriveSpeed = maxDriveSpeed; + return this; + } + + /** + * Set the gear ratio of the drivetrain. + * + *

The gear ratio is the ratio of the motor's rotational speed to the wheel's linear speed. + * This value should be set to the gear ratio of the drivetrain. + * + * @param driveGearRatio the gear ratio of the drivetrain + * @return this builder + */ + public DrivetrainConfigBuilder withDriveGearRatio(double driveGearRatio) { + this.driveGearRatio = driveGearRatio; + return this; + } + + /** + * Set the current at which the robot is slipping. + * + *

The slip current is the current at which the robot's wheels are slipping. This value + * should be set to the current at which the robot's wheels start to slip. + * + * @param slipCurrent the current at which the robot's wheels are slipping + * @return this builder + */ + public DrivetrainConfigBuilder withSlipCurrent(Current slipCurrent) { + this.slipCurrent = slipCurrent; + return this; + } + + /** + * Set whether the left side of the drivetrain should be inverted. + * + *

Inverting a side of the drivetrain means that the motor will turn in the opposite + * direction when given a positive output. This can be useful for drivetrains that have their + * motors mounted in a way that causes them to naturally turn in the opposite direction. + * + * @param invertLeftSide whether the left side should be inverted + * @return this builder + */ + public DrivetrainConfigBuilder withInvertLeftSide(boolean invertLeftSide) { + this.invertLeftSide = invertLeftSide; + return this; + } + + /** + * Set whether the right side of the drivetrain should be inverted. + * + * @param invertRightSide whether the right side should be inverted + * @return this builder + */ + public DrivetrainConfigBuilder withInvertRightSide(boolean invertRightSide) { + this.invertRightSide = invertRightSide; + return this; + } + + /** + * Set the moment of inertia of the drivetrain. + * + * @param driveInertia the moment of inertia of the drivetrain in kg*m^2 + * @return this builder + */ + public DrivetrainConfigBuilder withDriveInertia(MomentOfInertia driveInertia) { + this.driveInertia = driveInertia; + return this; + } + + /** + * Set the robot's mass. + * + * @param robotMass the mass of the robot in kg + * @return this builder + */ + public DrivetrainConfigBuilder withRobotMass(Mass robotMass) { + this.robotMass = robotMass; + return this; + } + + /** + * Set the type of the drivetrain. + * + * @param drivetrainType the type of the drivetrain + * @return this builder + */ + public DrivetrainConfigBuilder withDrivetrainType(String drivetrainType) { + this.drivetrainType = drivetrainType; + return this; + } + + /** + * Set the gyro used by the drivetrain. + * + * @param gyro the gyro used by the drivetrain + * @return this builder + */ + public DrivetrainConfigBuilder withGyro(GenericGyro gyro) { + this.gyro = gyro; + return this; + } + + /** + * Set the drive motor controller configuration. + * + * @param driveMotorControlConfig the drive motor controller configuration + * @return this builder + */ + public DrivetrainConfigBuilder withDriveMotorControlConfig( + SmartMotorControllerConfig driveMotorControlConfig) { + this.driveMotorControlConfig = driveMotorControlConfig; + return this; + } + + /** + * Set the canbus string for the drivetrain. + * + * @param canbus the canbus string for the drivetrain + * @return this builder + */ + public DrivetrainConfigBuilder withCanbus(String canbus) { + this.canbus = canbus; + return this; + } + + /** + * Set the bumper frame width of the drivetrain. + * + *

This value should be set to the width of the bumper frame in meters. This value is used by + * the drivetrain to calculate its linear velocity. + * + * @param bumperFrameWidth the bumper frame width of the drivetrain in meters + * @return this builder + */ + public DrivetrainConfigBuilder withBumperFrameWidth(Distance bumperFrameWidth) { + this.bumperFrameWidth = bumperFrameWidth; + return this; + } + + /** + * Set the bumper frame length of the drivetrain. + * + *

This value should be set to the length of the bumper frame in meters. This value is used + * by the drivetrain to calculate its linear velocity. + * + * @param bumperFrameLength the bumper frame length of the drivetrain in meters + * @return this builder + */ + public DrivetrainConfigBuilder withBumperFrameLength(Distance bumperFrameLength) { + this.bumperFrameLength = bumperFrameLength; + return this; + } + } +} diff --git a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java index edcc838a..08b774a5 100644 --- a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java @@ -28,12 +28,16 @@ import edu.wpi.first.wpilibj.smartdashboard.Field2d; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.DoubleSupplier; import java.util.function.Supplier; import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.GenericRobot.LogLevel; import org.frc5010.common.arch.GenericSubsystem; @@ -43,24 +47,36 @@ import org.frc5010.common.drive.pose.DrivePoseEstimator; import org.frc5010.common.sensors.Controller; import org.frc5010.common.telemetry.DisplayBoolean; -import org.ironmaple.simulation.SimulatedArena; import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.mechanism.LoggedMechanism2d; import org.littletonrobotics.junction.mechanism.LoggedMechanismLigament2d; import org.littletonrobotics.junction.mechanism.LoggedMechanismRoot2d; +import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.drivesims.AbstractDriveTrainSimulation; +import swervelib.simulation.ironmaple.simulation.drivesims.SwerveDriveSimulation; /** Generic class for defining drivetrain behavior */ public abstract class GenericDrivetrain extends GenericSubsystem { /** The pose estimator */ protected DrivePoseEstimator poseEstimator; + /** The robot velocity */ + @Getter @Setter private ChassisSpeeds robotVelocity = new ChassisSpeeds(); + + /** Returns the measured chassis speeds of the robot. */ + protected abstract ChassisSpeeds getChassisSpeeds(); /** Whether or not the robot is field oriented */ protected DisplayBoolean isFieldOrientedDrive; /** * Load the RobotConfig from the GUI settings. You should probably store this in your Constants * file */ - protected RobotConfig config; + protected RobotConfig ppRobotConfig; + + protected Supplier ppRobotConfigSupplier = () -> ppRobotConfig; + protected static Supplier> driveTrainSimulationSupplier = + () -> null; protected DisplayBoolean hasIssues; protected DoubleSupplier angleSpeedSupplier = null; public Supplier maxForwardAcceleration, @@ -90,10 +106,10 @@ public abstract class GenericDrivetrain extends GenericSubsystem { public GenericDrivetrain(LoggedMechanism2d mechVisual) { super(mechVisual); try { - config = RobotConfig.fromGUISettings(); + ppRobotConfig = RobotConfig.fromGUISettings(); } catch (Exception e) { // A default config in case the GUI settings can't be loaded - config = + ppRobotConfig = new RobotConfig( Kilogram.of(68).magnitude(), SingleJointedArmSim.estimateMOI(0.5, Kilogram.of(68).magnitude()), @@ -146,6 +162,19 @@ public Rotation2d getHeading() { return poseEstimator.getGyroRotation2d(); } + public ChassisSpeeds getFieldVelocity() { + return ChassisSpeeds.fromRobotRelativeSpeeds(robotVelocity, getHeading()); + } + + /** + * Returns the field-relative chassis acceleration. The base implementation returns zero; swerve + * drivetrains with motor acceleration signals override this. + * + * @return A ChassisSpeeds object with acceleration components in m/s² + */ + public ChassisSpeeds getFieldAcceleration() { + return new ChassisSpeeds(); + } /** * Drive with ChassisSpeeds * @@ -164,6 +193,7 @@ public abstract void driveWithFeedforwards( /** Updates the pose estimator in the periodic function. */ @Override public void periodic() { + setRobotVelocity(getChassisSpeeds()); hasIssues.setValue(hasIssues()); if (RobotBase.isSimulation() || useGlass) { updateGlassWidget(); @@ -256,6 +286,7 @@ public void simulationPeriodic() { int count = 0; List gpas = SimulatedArena.getInstance().getGamePiecesByType(Constants.Simulation.gamePieceA).stream() + .sorted(Comparator.comparingDouble(it -> it.getPose3d().getX() + it.getPose3d().getY())) .map(it -> it.getPose3d()) .collect(Collectors.toList()); for (Pose3d gpa : gpas) { @@ -292,6 +323,8 @@ protected void initializeSimulation(GenericDrivetrainConstants constants) { int count = 0; for (Pose3d gpa : SimulatedArena.getInstance().getGamePiecesByType(Constants.Simulation.gamePieceA).stream() + .sorted( + Comparator.comparingDouble(it -> it.getPose3d().getX() + it.getPose3d().getY())) .map(it -> it.getPose3d()) .collect(Collectors.toList())) { getField2d() @@ -307,6 +340,9 @@ protected void initializeSimulation(GenericDrivetrainConstants constants) { SimulatedArena.getInstance().getGamePiecesByType(Constants.Simulation.gamePieceB).stream() .map(it -> it.getPose3d()) .collect(Collectors.toList())) { + getField2d() + .getObject("CARPET" + count) + .setPose(new Pose2d(gpb.getX(), gpb.getY(), new Rotation2d())); getField2d() .getObject("GPB" + count) .setPose(new Pose2d(gpb.getX(), gpb.getY(), gpb.getRotation().toRotation2d())); @@ -431,4 +467,17 @@ public boolean hasIssues() { } return false; } + + /** + * Gets the maple-sim drivetrain simulation instance This is used to add intake simulation / + * launch game pieces from the robot + * + * @return an optional maple-sim {@link SwerveDriveSimulation} object, or {@link Optional#empty()} + * when calling from a real robot + */ + public static Optional getMapleSimDrive() { + return driveTrainSimulationSupplier.get(); + } + + public void addAutoCommands(LoggedDashboardChooser selectableCommand) {} } diff --git a/src/main/java/org/frc5010/common/drive/SimulatedDrivetrain.java b/src/main/java/org/frc5010/common/drive/SimulatedDrivetrain.java index 2c64c599..baf10449 100644 --- a/src/main/java/org/frc5010/common/drive/SimulatedDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/SimulatedDrivetrain.java @@ -87,7 +87,7 @@ public void setAutoBuilder() { new PIDConstants(5.0, 0.0, 0.0), // Translation PID constants new PIDConstants(5.0, 0.0, 0.0) // Rotation PID constants ), - config, // The robot configuration + ppRobotConfig, // The robot configuration () -> { // Boolean supplier that controls when the path will be mirrored for the red // alliance @@ -123,7 +123,6 @@ public void drive(ChassisSpeeds chassisSpeeds) { @Override public Field2d getField2d() { - // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getField2d'"); } } diff --git a/src/main/java/org/frc5010/common/drive/SwerveDriveConfig.java b/src/main/java/org/frc5010/common/drive/SwerveDriveConfig.java new file mode 100644 index 00000000..64182039 --- /dev/null +++ b/src/main/java/org/frc5010/common/drive/SwerveDriveConfig.java @@ -0,0 +1,248 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.drive; + +import edu.wpi.first.math.controller.SimpleMotorFeedforward; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.MomentOfInertia; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import java.util.Map; +import org.frc5010.common.config.UnitsParser; +import org.frc5010.common.config.json.devices.DeviceConfigReader; +import org.frc5010.common.config.json.devices.DrivetrainConstantsJson; +import org.frc5010.common.config.json.devices.DrivetrainConstantsJson.ModuleConfigJson; +import yams.gearing.GearBox; +import yams.gearing.MechanismGearing; +import yams.motorcontrollers.SmartMotorController; +import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; +import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; + +/*Configuration constants for the swerve drive */ +public class SwerveDriveConfig extends DrivetrainConfig { + /* The gear ratio of the steer motor */ + protected double steerGearRatio; + /* The configuration for the steer motor controller */ + protected SmartMotorControllerConfig steerMotorControlConfig; + /** + * The ratio of the drive motor to the steer motor. Every 1 rotation of the azimuth results in + * kCoupleRatio drive motor turns; + */ + protected double coupleRatio; + /* The steer inertia constant */ + protected MomentOfInertia steerInertia; + /** + * The module configurations, mapped by module name which should be frontLeft, frontRight, + * backLeft, backRight + */ + protected Map modules; + + protected SwerveDriveConfig() {} + + public SwerveDriveConfig(SwerveDriveConfigBuilder builder) { + super(builder); + this.wheelBase = builder.wheelBase; + this.coupleRatio = builder.coupleRatio; + this.steerGearRatio = builder.steerGearRatio; + this.steerMotorControlConfig = builder.steerMotorControlConfig; + this.steerInertia = builder.steerInertia; + this.modules = builder.modules; + } + + public static SwerveDriveConfigBuilder builder() { + return new SwerveDriveConfigBuilder(); + } + + public static class SwerveDriveConfigBuilder extends DrivetrainConfig.DrivetrainConfigBuilder { + protected Distance wheelBase; + protected double steerGearRatio; + protected SmartMotorControllerConfig steerMotorControlConfig; + protected double coupleRatio; + protected MomentOfInertia steerInertia; + protected Map modules; + + public SwerveDriveConfigBuilder() {} + + public SwerveDriveConfig build(DrivetrainConstantsJson constants, SubsystemBase subsystem) { + super.build(constants, subsystem); + + withSteerGearRatio( + new MechanismGearing(GearBox.fromStages(constants.steerGearRatio)) + .getRotorToMechanismRatio()); + withCoupleRatio(constants.coupleRatio); + withSteerInertia(UnitsParser.parseMomentOfInertia(constants.steerInertia)); + withWheelBase(UnitsParser.parseDistance(constants.wheelBase)); + + for (ModuleConfigJson moduleConfig : constants.modules.values()) { + modules.put( + moduleConfig.toString(), + new SwerveModuleConfig() + .withDriveMotorInverted(moduleConfig.driveMotorSetup.inverted) + .withSteerMotorInverted(moduleConfig.steerMotorSetup.inverted) + .withEncoderInverted(moduleConfig.encoderInverted) + .withEncoderId(moduleConfig.encoderId) + .withAbsoluteOffset(UnitsParser.parseAngle(moduleConfig.absoluteOffset)) + .withDriveMotor( + DeviceConfigReader.getSmartMotor( + moduleConfig.driveMotorSetup.controllerType, + moduleConfig.driveMotorSetup.motorType, + moduleConfig.driveMotorSetup.canId, + new SmartMotorControllerConfig(subsystem) + .withControlMode(ControlMode.CLOSED_LOOP) + .withClosedLoopController( + constants.driveMotorControl.feedBack.p, + constants.driveMotorControl.feedBack.i, + constants.driveMotorControl.feedBack.d) + .withTelemetry( + moduleConfig.driveMotorSetup.name + " DriveMotor", + TelemetryVerbosity.MID) + .withFeedforward( + new SimpleMotorFeedforward( + constants.driveMotorControl.feedForward.s, + constants.driveMotorControl.feedForward.v, + constants.driveMotorControl.feedForward.a)), + canbus) + .orElse(null)) + .withSteerMotor( + DeviceConfigReader.getSmartMotor( + moduleConfig.steerMotorSetup.controllerType, + moduleConfig.steerMotorSetup.motorType, + moduleConfig.steerMotorSetup.canId, + new SmartMotorControllerConfig(subsystem) + .withControlMode(ControlMode.CLOSED_LOOP) + .withClosedLoopController( + constants.steerMotorControl.feedBack.p, + constants.steerMotorControl.feedBack.i, + constants.steerMotorControl.feedBack.d) + .withTelemetry( + moduleConfig.steerMotorSetup.name + " SteerMotor", + TelemetryVerbosity.MID) + .withFeedforward( + new SimpleMotorFeedforward( + constants.steerMotorControl.feedForward.s, + constants.steerMotorControl.feedForward.v, + constants.steerMotorControl.feedForward.a)), + canbus) + .orElse(null))); + } + + return new SwerveDriveConfig(this); + } + + public SwerveDriveConfigBuilder withSteerGearRatio(double steerGearRatio) { + this.steerGearRatio = steerGearRatio; + return this; + } + + public SwerveDriveConfigBuilder withSteerMotorControlConfig( + SmartMotorControllerConfig steerMotorControlConfig) { + this.steerMotorControlConfig = steerMotorControlConfig; + return this; + } + + public SwerveDriveConfigBuilder withCoupleRatio(double coupleRatio) { + this.coupleRatio = coupleRatio; + return this; + } + + public SwerveDriveConfigBuilder withSteerInertia(MomentOfInertia steerInertia) { + this.steerInertia = steerInertia; + return this; + } + + public SwerveDriveConfigBuilder withModules(Map modules) { + this.modules = modules; + return this; + } + + /** + * Set the wheel base of the drivetrain. + * + *

This value should be set to the distance between the centers of the left and right wheels + * on the drivetrain in meters. This value is used by the drivetrain to calculate its linear + * velocity. + * + * @param wheelBase the wheel base of the drivetrain in meters + * @return this builder + */ + public DrivetrainConfigBuilder withWheelBase(Distance wheelBase) { + this.wheelBase = wheelBase; + return this; + } + } + + public static class SwerveModuleConfig { + protected SmartMotorController driveMotor; + protected SmartMotorController steerMotor; + protected int encoderId; + + protected Angle absoluteOffset; + protected boolean steerMotorInverted; + protected boolean driveMotorInverted; + protected boolean encoderInverted; + + public SwerveModuleConfig() {} + + public SwerveModuleConfig withDriveMotor(SmartMotorController driveMotor) { + this.driveMotor = driveMotor; + return this; + } + + public SwerveModuleConfig withSteerMotor(SmartMotorController steerMotor) { + this.steerMotor = steerMotor; + return this; + } + + public SwerveModuleConfig withEncoderId(int encoderId) { + this.encoderId = encoderId; + return this; + } + + public SwerveModuleConfig withAbsoluteOffset(Angle absoluteOffset) { + this.absoluteOffset = absoluteOffset; + return this; + } + + public SwerveModuleConfig withSteerMotorInverted(boolean steerMotorInverted) { + this.steerMotorInverted = steerMotorInverted; + return this; + } + + public SwerveModuleConfig withDriveMotorInverted(boolean driveMotorInverted) { + this.driveMotorInverted = driveMotorInverted; + return this; + } + + public SwerveModuleConfig withEncoderInverted(boolean encoderInverted) { + this.encoderInverted = encoderInverted; + return this; + } + } + + public Map getModules() { + return modules; + } + + public double getSteerGearRatio() { + return steerGearRatio; + } + + public SmartMotorControllerConfig getSteerMotorControlConfig() { + return steerMotorControlConfig; + } + + public double getCoupleRatio() { + return coupleRatio; + } + + public MomentOfInertia getSteerInertia() { + return steerInertia; + } + + public Distance getWheelBase() { + return wheelBase; + } +} diff --git a/src/main/java/org/frc5010/common/drive/pose/DrivePoseEstimator.java b/src/main/java/org/frc5010/common/drive/pose/DrivePoseEstimator.java index fe39ab93..23e54c42 100644 --- a/src/main/java/org/frc5010/common/drive/pose/DrivePoseEstimator.java +++ b/src/main/java/org/frc5010/common/drive/pose/DrivePoseEstimator.java @@ -232,8 +232,6 @@ private void resetProviderPoses(Pose2d pose) { * Env providers if the pose is high caliber and close ENABLED_ENV - reads the pose from the Env * providers and expects given pose to be correct ALL - reads and fuses poses from both Env and * Field sources, resetting Env pose based on update - * - * @return the command that updates the pose estimator */ protected void updatePoseObservationFromProviders() { poseTracker.updateLocalMeasurements(); diff --git a/src/main/java/org/frc5010/common/drive/pose/PoseProvider.java b/src/main/java/org/frc5010/common/drive/pose/PoseProvider.java index bf56bc0e..9d023423 100644 --- a/src/main/java/org/frc5010/common/drive/pose/PoseProvider.java +++ b/src/main/java/org/frc5010/common/drive/pose/PoseProvider.java @@ -18,8 +18,8 @@ import org.littletonrobotics.junction.AutoLog; import org.littletonrobotics.junction.Logger; -/** Add your docs here. */ public interface PoseProvider { + public VisionIOInputsAutoLogged input = new VisionIOInputsAutoLogged(); public Alert disconnectedAlert = new Alert("PoseProvider", AlertType.kWarning); public int cameraIndex = 0; diff --git a/src/main/java/org/frc5010/common/drive/pose/AkitSwervePose.java b/src/main/java/org/frc5010/common/drive/pose/SwerveFunctionsPose.java similarity index 80% rename from src/main/java/org/frc5010/common/drive/pose/AkitSwervePose.java rename to src/main/java/org/frc5010/common/drive/pose/SwerveFunctionsPose.java index 21fb0e60..c6ee9e60 100644 --- a/src/main/java/org/frc5010/common/drive/pose/AkitSwervePose.java +++ b/src/main/java/org/frc5010/common/drive/pose/SwerveFunctionsPose.java @@ -9,13 +9,12 @@ import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; -import org.frc5010.common.drive.swerve.akit.AkitSwerveDrive; +import org.frc5010.common.drive.swerve.SwerveDriveFunctions; -/** Add your docs here. */ -public class AkitSwervePose extends GenericPose { - private final AkitSwerveDrive drivetrain; +public class SwerveFunctionsPose extends GenericPose { + private SwerveDriveFunctions drivetrain; - public AkitSwervePose(AkitSwerveDrive drivetrain) { + public SwerveFunctionsPose(SwerveDriveFunctions drivetrain) { super(null); this.drivetrain = drivetrain; field2d = drivetrain.getField2d(); @@ -30,7 +29,7 @@ public void resetEncoders() { @Override public void updateVisionMeasurements( Pose2d robotPose, double imageCaptureTime, Matrix stdVector) { - visionConsumer.accept(robotPose, imageCaptureTime, stdVector); + drivetrain.addVisionMeasurement(robotPose, imageCaptureTime, stdVector); } @Override diff --git a/src/main/java/org/frc5010/common/drive/pose/YAGSLSwervePose.java b/src/main/java/org/frc5010/common/drive/pose/YAGSLSwervePose.java deleted file mode 100644 index d4e61751..00000000 --- a/src/main/java/org/frc5010/common/drive/pose/YAGSLSwervePose.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package org.frc5010.common.drive.pose; - -import edu.wpi.first.math.Matrix; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.numbers.N1; -import edu.wpi.first.math.numbers.N3; -import org.frc5010.common.drive.swerve.YAGSLSwerveDrivetrain; - -public class YAGSLSwervePose extends GenericPose { - private YAGSLSwerveDrivetrain drivetrain; - - public YAGSLSwervePose(YAGSLSwerveDrivetrain drivetrain) { - super(null); - this.drivetrain = drivetrain; - field2d = drivetrain.getField2d(); - visionConsumer = drivetrain::updateVisionMeasurements; - } - - @Override - public void resetEncoders() { - drivetrain.resetEncoders(); - } - - @Override - public void updateVisionMeasurements( - Pose2d robotPose, double imageCaptureTime, Matrix stdVector) { - drivetrain.updateVisionMeasurements(robotPose, imageCaptureTime, stdVector); - } - - @Override - public void updateRobotPoseOnField(Pose2d pose) { - field2d.setRobotPose(pose); - } - - @Override - public void updateLocalMeasurements() {} - - @Override - public Pose2d getCurrentPose() { - return drivetrain.getPose(); - } - - @Override - public void resetToPose(Pose2d pose) { - drivetrain.resetOdometry(pose); - } - - @Override - public Rotation2d getGyroRotation2d() { - return drivetrain.getHeading(); - } -} diff --git a/src/main/java/org/frc5010/common/drive/swerve/AkitSwerveConfig.java b/src/main/java/org/frc5010/common/drive/swerve/AkitSwerveConfig.java new file mode 100644 index 00000000..dec8fb73 --- /dev/null +++ b/src/main/java/org/frc5010/common/drive/swerve/AkitSwerveConfig.java @@ -0,0 +1,239 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.drive.swerve; + +import static edu.wpi.first.units.Units.Amps; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.CurrentLimitsConfigs; +import com.ctre.phoenix6.configs.Slot0Configs; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.signals.StaticFeedforwardSignValue; +import com.ctre.phoenix6.swerve.SwerveDrivetrainConstants; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.ClosedLoopOutputType; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.DriveMotorArrangement; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.SteerFeedbackType; +import com.ctre.phoenix6.swerve.SwerveModuleConstants.SteerMotorArrangement; +import com.ctre.phoenix6.swerve.SwerveModuleConstantsFactory; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import frc.robot.Constants; +import org.frc5010.common.config.UnitsParser; +import org.frc5010.common.config.json.devices.DrivetrainConstantsJson; +import org.frc5010.common.drive.SwerveDriveConfig; +import yams.gearing.GearBox; +import yams.gearing.MechanismGearing; + +/** Add your docs here. */ +public class AkitSwerveConfig extends SwerveDriveConfig { + public final SwerveDrivetrainConstants DrivetrainConstants; + + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontLeft; + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontRight; + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackLeft; + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackRight; + public double ODOMETRY_FREQUENCY; + public final double DRIVE_BASE_RADIUS; + protected SwerveModuleConstants[] MODULES; + private final CANBus kCANBus; + + public AkitSwerveConfig(AkitTalonFXSwerveConfigBuilder builder) { + super(builder); + this.DrivetrainConstants = builder.DrivetrainConstants; + this.FrontLeft = builder.FrontLeft; + this.FrontRight = builder.FrontRight; + this.BackLeft = builder.BackLeft; + this.BackRight = builder.BackRight; + MODULES = new SwerveModuleConstants[] {FrontLeft, FrontRight, BackLeft, BackRight}; + this.kCANBus = builder.kCANBus; + DRIVE_BASE_RADIUS = + Math.max( + Math.max( + Math.hypot(FrontLeft.LocationX, FrontLeft.LocationY), + Math.hypot(FrontRight.LocationX, FrontRight.LocationY)), + Math.max( + Math.hypot(BackLeft.LocationX, BackLeft.LocationY), + Math.hypot(BackRight.LocationX, BackRight.LocationY))); + } + + public SwerveModuleConstants getModuleConstants(int index) { + return MODULES[index]; + } + + public static AkitSwerveConfig builder( + DrivetrainConstantsJson constants, SubsystemBase subsystem) { + return new AkitSwerveConfig(new AkitTalonFXSwerveConfigBuilder(constants, subsystem)); + } + + public CANBus getCANBus() { + return kCANBus; + } + + public static class AkitTalonFXSwerveConfigBuilder + extends SwerveDriveConfig.SwerveDriveConfigBuilder { + public final SwerveDrivetrainConstants DrivetrainConstants; + + private final SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + ConstantCreator; + + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontLeft; + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + FrontRight; + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackLeft; + public final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + BackRight; + public final CANBus kCANBus; + + public AkitTalonFXSwerveConfigBuilder( + DrivetrainConstantsJson constants, SubsystemBase subsystem) { + super(); + kCANBus = new CANBus(constants.canbus, "./logs/example.hoot"); + bumperFrameLength = UnitsParser.parseDistance(constants.bumperFrameLength); + bumperFrameWidth = UnitsParser.parseDistance(constants.bumperFrameWidth); + maxDriveSpeed = UnitsParser.parseVelocity(constants.maxDriveSpeed); + robotMass = UnitsParser.parseMass(constants.robotMass); + driveInertia = UnitsParser.parseMomentOfInertia(constants.driveInertia); + steerInertia = UnitsParser.parseMomentOfInertia(constants.steerInertia); + canbus = Constants.CURRENT_MODE == Constants.SIM_MODE ? "" : constants.canbus; + wheelDiameter = UnitsParser.parseDistance(constants.wheelDiameter); + driveGearRatio = + new MechanismGearing(GearBox.fromStages(constants.driveGearRatio)) + .getRotorToMechanismRatio(); + steerGearRatio = + new MechanismGearing(GearBox.fromStages(constants.steerGearRatio)) + .getRotorToMechanismRatio(); + DrivetrainConstants = + new SwerveDrivetrainConstants() + .withCANBusName(kCANBus.getName()) + .withPigeon2Id(constants.gyro.id) + .withPigeon2Configs(null); + + CANcoderConfiguration encoderInitialConfigs = new CANcoderConfiguration(); + ConstantCreator = + new SwerveModuleConstantsFactory< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration>() + .withDriveMotorGearRatio( + new MechanismGearing(GearBox.fromStages(constants.driveGearRatio)) + .getRotorToMechanismRatio()) + .withSteerMotorGearRatio( + new MechanismGearing(GearBox.fromStages(constants.steerGearRatio)) + .getRotorToMechanismRatio()) + .withCouplingGearRatio(constants.coupleRatio) + .withWheelRadius(UnitsParser.parseDistance(constants.wheelDiameter).div(2)) + .withSteerMotorGains( + new Slot0Configs() + .withKP(constants.steerMotorControl.feedBack.p) + .withKI(constants.steerMotorControl.feedBack.i) + .withKD(constants.steerMotorControl.feedBack.d) + .withKS(constants.steerMotorControl.feedForward.s) + .withKV(constants.steerMotorControl.feedForward.v) + .withKA(constants.steerMotorControl.feedForward.a) + .withStaticFeedforwardSign(StaticFeedforwardSignValue.UseClosedLoopSign)) + .withDriveMotorGains( + new Slot0Configs() + .withKP(constants.driveMotorControl.feedBack.p) + .withKI(constants.driveMotorControl.feedBack.i) + .withKD(constants.driveMotorControl.feedBack.d) + .withKS(constants.driveMotorControl.feedForward.s) + .withKV(constants.driveMotorControl.feedForward.v) + .withKA(constants.driveMotorControl.feedForward.a)) + .withSteerMotorClosedLoopOutput(ClosedLoopOutputType.Voltage) + .withDriveMotorClosedLoopOutput(ClosedLoopOutputType.Voltage) + .withSlipCurrent(UnitsParser.parseAmps(constants.slipCurrent)) + .withSpeedAt12Volts(UnitsParser.parseVelocity(constants.maxDriveSpeed)) + .withDriveMotorType(DriveMotorArrangement.TalonFX_Integrated) + .withSteerMotorType(SteerMotorArrangement.TalonFX_Integrated) + .withFeedbackSource(SteerFeedbackType.FusedCANcoder) + .withDriveMotorInitialConfigs(new TalonFXConfiguration()) + .withSteerMotorInitialConfigs( + new TalonFXConfiguration() + .withCurrentLimits( + new CurrentLimitsConfigs() + // Swerve azimuth does not require much torque output, so we can set + // arelatively low stator current limit to help avoid brownouts + // without impacting performance. + .withStatorCurrentLimit(Amps.of(30)) + .withStatorCurrentLimitEnable(true))) + .withEncoderInitialConfigs(encoderInitialConfigs) + .withSteerInertia(UnitsParser.parseMomentOfInertia(constants.steerInertia)) + .withDriveInertia(UnitsParser.parseMomentOfInertia(constants.driveInertia)) + .withSteerFrictionVoltage(constants.steerMotorControl.feedForward.s) + .withDriveFrictionVoltage(constants.driveMotorControl.feedForward.s); + trackWidth = UnitsParser.parseDistance(constants.trackWidth); + wheelBase = UnitsParser.parseDistance(constants.wheelBase); + Distance kFrontLeftXPos = trackWidth.div(2); + Distance kFrontLeftYPos = wheelBase.div(2); + FrontLeft = + ConstantCreator.createModuleConstants( + constants.modules.get("frontLeft").steerMotorSetup.canId, + constants.modules.get("frontLeft").driveMotorSetup.canId, + constants.modules.get("frontLeft").encoderId, + UnitsParser.parseAngle(constants.modules.get("frontLeft").absoluteOffset), + kFrontLeftXPos, + kFrontLeftYPos, + constants.invertLeftSide, + constants.modules.get("frontLeft").steerMotorSetup.inverted, + constants.modules.get("frontLeft").encoderInverted); + Distance kFrontRightXPos = trackWidth.div(2); + Distance kFrontRightYPos = wheelBase.div(-2); + FrontRight = + ConstantCreator.createModuleConstants( + constants.modules.get("frontRight").steerMotorSetup.canId, + constants.modules.get("frontRight").driveMotorSetup.canId, + constants.modules.get("frontRight").encoderId, + UnitsParser.parseAngle(constants.modules.get("frontRight").absoluteOffset), + kFrontRightXPos, + kFrontRightYPos, + constants.invertRightSide, + constants.modules.get("frontRight").steerMotorSetup.inverted, + constants.modules.get("frontRight").encoderInverted); + + Distance kBackLeftXPos = trackWidth.div(-2); + Distance kBackLeftYPos = wheelBase.div(2); + BackLeft = + ConstantCreator.createModuleConstants( + constants.modules.get("backLeft").steerMotorSetup.canId, + constants.modules.get("backLeft").driveMotorSetup.canId, + constants.modules.get("backLeft").encoderId, + UnitsParser.parseAngle(constants.modules.get("backLeft").absoluteOffset), + kBackLeftXPos, + kBackLeftYPos, + constants.invertLeftSide, + constants.modules.get("backLeft").steerMotorSetup.inverted, + constants.modules.get("backLeft").encoderInverted); + + Distance kBackRightXPos = trackWidth.div(-2); + Distance kBackRightYPos = wheelBase.div(-2); + BackRight = + ConstantCreator.createModuleConstants( + constants.modules.get("backRight").steerMotorSetup.canId, + constants.modules.get("backRight").driveMotorSetup.canId, + constants.modules.get("backRight").encoderId, + UnitsParser.parseAngle(constants.modules.get("backRight").absoluteOffset), + kBackRightXPos, + kBackRightYPos, + constants.invertRightSide, + constants.modules.get("backRight").steerMotorSetup.inverted, + constants.modules.get("backRight").encoderInverted); + } + } +} diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index 011b1600..b206e86a 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -15,7 +15,6 @@ import com.pathplanner.lib.auto.AutoBuilder; import com.pathplanner.lib.commands.PathfindingCommand; import com.pathplanner.lib.config.PIDConstants; -import com.pathplanner.lib.config.RobotConfig; import com.pathplanner.lib.controllers.PPHolonomicDriveController; import com.pathplanner.lib.path.PathConstraints; import com.pathplanner.lib.util.DriveFeedforwards; @@ -37,12 +36,12 @@ import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.DriverStation.Alliance; import edu.wpi.first.wpilibj.Timer; -import edu.wpi.first.wpilibj.XboxController; import edu.wpi.first.wpilibj.smartdashboard.Field2d; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj.util.Color8Bit; import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.Commands; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; @@ -56,12 +55,14 @@ import org.frc5010.common.constants.GenericDrivetrainConstants; import org.frc5010.common.constants.RobotConstantsDef; import org.frc5010.common.drive.GenericDrivetrain; +import org.frc5010.common.drive.swerve.akit.AkitSwerveDrive; import org.frc5010.common.drive.swerve_utils.PathConstraints5010; import org.frc5010.common.drive.swerve_utils.SwerveSetpointGenerator5010; import org.frc5010.common.sensors.Controller; import org.json.simple.parser.ParseException; import org.littletonrobotics.junction.mechanism.LoggedMechanism2d; import org.littletonrobotics.junction.mechanism.LoggedMechanismLigament2d; +import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; /** Add your docs here. */ public class GenericSwerveDrivetrain extends GenericDrivetrain { @@ -77,8 +78,10 @@ public GenericSwerveDrivetrain( this.swerveDrive = swerveDriveFunctions; this.swerveConstants = swerveConstants; + ppRobotConfigSupplier = swerveDrive.getPPRobotConfigSupplier(); setDrivetrainPoseEstimator(swerveDrive.initializePoseEstimator()); initializeSimulation(swerveConstants); + driveTrainSimulationSupplier = swerveDrive.getDriveTrainSimulationSupplier(); } @Override @@ -114,7 +117,7 @@ public void setupPathPlanner() { new PIDConstants(4, 0, 0), // Translation PID constants new PIDConstants(1.0, 0, 0) // Rotation PID constants ), - config, // The robot configuration + ppRobotConfigSupplier.get(), // The robot configuration () -> { // Boolean supplier that controls when the path will be mirrored for the red // alliance @@ -129,7 +132,12 @@ public void setupPathPlanner() { // Preload PathPlanner Path finding // IF USING CUSTOM PATHFINDER ADD BEFORE THIS LINE - PathfindingCommand.warmupCommand().schedule(); + CommandScheduler.getInstance().schedule(PathfindingCommand.warmupCommand()); + } + + @Override + public void addAutoCommands(LoggedDashboardChooser selectableCommand) { + swerveDrive.addAutoCommands(selectableCommand, this); } @Override @@ -143,10 +151,10 @@ public void updateGlassWidget() { motorDials.get(moduleKey).setAngle(turningDeg + 90); motorDials .get(moduleKey) - .setLength(0.0001 * modules[moduleKey].steerVelocityDegreesPerSecond() + 0.002); + .setLength(0.1 * modules[moduleKey].steerVelocityDegreesPerSecond() + 0.02); expectDials .get(moduleKey) - .setLength(0.0001 * modules[moduleKey].driveVelocityMetersPerSecond() + 0.002); + .setLength(0.1 * modules[moduleKey].driveVelocityMetersPerSecond() + 0.02); expectDials.get(moduleKey).setAngle(modules[moduleKey].expectedSteerDegrees() + 90); } } @@ -159,6 +167,11 @@ public GenericDrivetrainConstants getSwerveConstants() { return swerveConstants; } + @Override + protected ChassisSpeeds getChassisSpeeds() { + return swerveDrive.getRobotVelocity(); + } + @Override public void initGlassWidget(GenericDrivetrainConstants constants) { SmartDashboard.putData("Drive Visual", mechanismSimulation); @@ -218,7 +231,7 @@ public void initGlassWidget(GenericDrivetrainConstants constants) { .get(i) .append( new LoggedMechanismLigament2d( - i + "-Abs", 0.10, 90, 6, new Color8Bit(Color.kBlue)))); + i + "-Abs", 0.50, 90, 6, new Color8Bit(Color.kBlue)))); expectDials.put( i, visualRoots @@ -256,6 +269,20 @@ public ChassisSpeeds getFieldVelocity() { return swerveDrive.getFieldVelocity(); } + /** + * Gets the current field-relative chassis acceleration derived from drive motor acceleration + * signals. Returns zero if the underlying drive implementation does not support acceleration + * signals. + * + * @return A ChassisSpeeds object representing field-relative acceleration (m/s² components) + */ + public ChassisSpeeds getFieldAcceleration() { + if (swerveDrive instanceof AkitSwerveDrive) { + return ((AkitSwerveDrive) swerveDrive).getFieldAcceleration(); + } + return new ChassisSpeeds(); + } + /** * Gets the current pose (position and rotation) of the robot, as reported by odometry. * @@ -350,7 +377,7 @@ public Supplier driveToPosePrecise( new PIDConstants(2.0, 0, 0.0), // Translation PID constants new PIDConstants(1.0, 0, 0.0) // Rotation PID constants ), - config, + ppRobotConfigSupplier.get(), this) .beforeStarting( () -> { @@ -441,7 +468,7 @@ public Supplier driveToPoseAuton( new PIDConstants(4.0, 0, 0.0), // Translation PID constants new PIDConstants(4.0, 0, 0.0) // Rotation PID constants ), - config, + ppRobotConfigSupplier.get(), this) .beforeStarting( () -> { @@ -506,7 +533,7 @@ public Supplier newDriveToPoseAuton( new PIDConstants(4.0, 0, 0.0), // Translation PID constants new PIDConstants(4.0, 0, 0.0) // Rotation PID constants ), - config, + ppRobotConfigSupplier.get(), this) .beforeStarting( () -> { @@ -611,7 +638,7 @@ public Command driveWithSetpointGenerator(Supplier robotRelativeC throws IOException, ParseException { SwerveSetpointGenerator5010 setpointGenerator = new SwerveSetpointGenerator5010( - RobotConfig.fromGUISettings(), + ppRobotConfigSupplier.get(), swerveDrive.getMaximumModuleAngleVelocity().in(RadiansPerSecond)); AtomicReference prevSetpoint = @@ -713,9 +740,9 @@ public Command driveWithSetpointGeneratorOrientationConsidered( } public Command createDefaultCommand(Controller driverXbox) { - DoubleSupplier leftX = () -> driverXbox.getAxisValue(XboxController.Axis.kLeftX.value); - DoubleSupplier leftY = () -> driverXbox.getAxisValue(XboxController.Axis.kLeftY.value); - DoubleSupplier rightX = () -> driverXbox.getAxisValue(XboxController.Axis.kRightX.value); + DoubleSupplier leftX = () -> driverXbox.getLeftXAxis(); + DoubleSupplier leftY = () -> driverXbox.getLeftYAxis(); + DoubleSupplier rightX = () -> driverXbox.getRightXAxis(); BooleanSupplier isFieldOriented = () -> isFieldOrientedDrive.getValue(); /** @@ -736,12 +763,12 @@ public Command createDefaultCommand(Controller driverXbox) { } public Command createDefaultTestCommand(Controller driverXbox) { - DoubleSupplier leftX = () -> driverXbox.getAxisValue(XboxController.Axis.kLeftX.value); - DoubleSupplier leftY = () -> driverXbox.getAxisValue(XboxController.Axis.kLeftY.value); - DoubleSupplier rightX = () -> driverXbox.getAxisValue(XboxController.Axis.kRightX.value); + DoubleSupplier leftX = () -> driverXbox.getLeftXAxis(); + DoubleSupplier leftY = () -> driverXbox.getLeftYAxis(); + DoubleSupplier rightX = () -> driverXbox.getRightXAxis(); BooleanSupplier isFieldOriented = () -> isFieldOrientedDrive.getValue(); - // driverXbox.createAButton().whileTrue(sysIdDriveMotorCommand()); + driverXbox.createAButton().whileTrue(sysIdDriveMotorCommand()); // driverXbox.createBButton().whileTrue(sysIdAngleMotorCommand()); // return Commands.run(() -> SwerveDriveTest.centerModules(swerveDrive), this); return new JoystickToSwerve( diff --git a/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java b/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java index f2f84dee..ba3ac82d 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java +++ b/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java @@ -4,22 +4,42 @@ package org.frc5010.common.drive.swerve; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.ppConfig; + +import com.pathplanner.lib.config.RobotConfig; import com.pathplanner.lib.util.DriveFeedforwards; +import edu.wpi.first.math.Matrix; import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.kinematics.ChassisSpeeds; import edu.wpi.first.math.kinematics.SwerveModulePosition; import edu.wpi.first.math.kinematics.SwerveModuleState; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.units.measure.Force; import edu.wpi.first.wpilibj.smartdashboard.Field2d; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.SubsystemBase; +import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import org.frc5010.common.drive.pose.DrivePoseEstimator; +import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; import swervelib.SwerveDrive; +import swervelib.simulation.ironmaple.simulation.drivesims.AbstractDriveTrainSimulation; +import swervelib.simulation.ironmaple.simulation.drivesims.SwerveDriveSimulation; +import swervelib.simulation.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; /** Add your docs here. */ public abstract class SwerveDriveFunctions { + public static DriveTrainSimulationConfig mapleSimConfig = DriveTrainSimulationConfig.Default(); + public static SwerveDriveSimulation driveSimulation = null; + + public static final Lock odometryLock = new ReentrantLock(); + protected GenericSwerveModuleInfo[] moduleInfos; public abstract GenericSwerveModuleInfo[] getModulesInfo(); @@ -55,6 +75,11 @@ public abstract class SwerveDriveFunctions { public abstract Command sysIdAngleMotorCommand(SubsystemBase swerveSubsystem); + public abstract void addVisionMeasurement( + Pose2d visionRobotPoseMeters, + double timestampSeconds, + Matrix visionMeasurementStdDevs); + /** * Drive the robot using the {@link SwerveModuleState}, it is recommended to have {@link * SwerveDrive#setCosineCompensator(boolean)} set to false for this.
@@ -69,6 +94,12 @@ public abstract void drive( public abstract Pose2d getPose(); + public abstract void setPose(Pose2d pose); + + public abstract Rotation2d getRotation(); + + public abstract Pose2d getSimPose(); + /** * Gets the current module states (azimuth and velocity) * @@ -94,4 +125,19 @@ public void periodic() { public void updateSimulation() { // Default empty } + + public void addAutoCommands( + LoggedDashboardChooser selectableCommand, GenericSwerveDrivetrain drivetrain) { + // Default empty + } + + public Supplier getPPRobotConfigSupplier() { + return () -> ppConfig; + } + + public void setPPRobotConfigSupplier(Supplier robotConfigSupplier) {} + + public Supplier> getDriveTrainSimulationSupplier() { + return () -> Optional.ofNullable(driveSimulation); + } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java index bac1a956..960992a4 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java @@ -38,10 +38,12 @@ import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Config; import java.io.File; import java.util.Arrays; +import java.util.Optional; import java.util.function.DoubleSupplier; +import java.util.function.Supplier; import org.frc5010.common.constants.GenericDrivetrainConstants; import org.frc5010.common.drive.pose.DrivePoseEstimator; -import org.frc5010.common.drive.pose.YAGSLSwervePose; +import org.frc5010.common.drive.pose.SwerveFunctionsPose; import swervelib.SwerveController; import swervelib.SwerveDrive; import swervelib.SwerveDriveTest; @@ -49,6 +51,7 @@ import swervelib.math.SwerveMath; import swervelib.parser.SwerveDriveConfiguration; import swervelib.parser.SwerveParser; +import swervelib.simulation.ironmaple.simulation.drivesims.AbstractDriveTrainSimulation; import swervelib.telemetry.SwerveDriveTelemetry; import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; @@ -118,23 +121,6 @@ public YAGSLSwerveDrivetrain( // offsets onto it. Throws warning if not possible /** 5010 Code */ - // SwerveConstants swerveConstants = (SwerveConstants) constants; - // if (swerveConstants.getSwerveModuleConstants().getDriveFeedForward().size() > - // 0) { - // Map motorFFMap = - // swerveConstants.getSwerveModuleConstants().getDriveFeedForward(); - // Map swerveModuleMap = swerveDrive.getModuleMap(); - // motorFFMap.keySet().stream() - // .forEach( - // module -> { - // MotorFeedFwdConstants ff = motorFFMap.get(module); - // double kS = ff.getkS(); - // double kV = ff.getkV(); - // double kA = ff.getkA(); - // swerveModuleMap.get(module).setFeedforward(new SimpleMotorFeedforward(kS, kV, - // kA)); - // }); - // } } /** @@ -144,7 +130,7 @@ public YAGSLSwerveDrivetrain( */ @Override public DrivePoseEstimator initializePoseEstimator() { - return new DrivePoseEstimator(new YAGSLSwervePose(this)); + return new DrivePoseEstimator(new SwerveFunctionsPose(this)); } @Override @@ -199,8 +185,8 @@ public Command sysIdAngleMotorCommand(SubsystemBase swerveSubsystem) { return SwerveDriveTest.generateSysIdCommand( SwerveDriveTest.setAngleSysIdRoutine(new Config(), swerveSubsystem, swerveDrive), 3.0, - 5.0, - 3.0); + 3.0, + 1.0); } /** @@ -406,7 +392,8 @@ public SwerveDriveKinematics getKinematics() { * * @param initialHolonomicPose The pose to set the odometry to */ - public void resetOdometry(Pose2d initialHolonomicPose) { + @Override + public void setPose(Pose2d initialHolonomicPose) { swerveDrive.resetOdometry(initialHolonomicPose); } @@ -454,7 +441,7 @@ public void zeroGyroWithAlliance() { if (isRedAlliance()) { zeroGyro(); // Set the pose 180 degrees - resetOdometry(new Pose2d(getPose().getTranslation(), Rotation2d.fromDegrees(180))); + setPose(new Pose2d(getPose().getTranslation(), Rotation2d.fromDegrees(180))); } else { zeroGyro(); } @@ -476,7 +463,8 @@ public void setMotorBrake(boolean brake) { * * @return The yaw angle */ - public Rotation2d getHeading() { + @Override + public Rotation2d getRotation() { return getPose().getRotation(); } @@ -495,7 +483,7 @@ public ChassisSpeeds getTargetSpeeds( xInput = Math.pow(xInput, 3); yInput = Math.pow(yInput, 3); return swerveDrive.swerveController.getTargetSpeeds( - xInput, yInput, headingX, headingY, getHeading().getRadians(), maximumSpeed); + xInput, yInput, headingX, headingY, getRotation().getRadians(), maximumSpeed); } /** @@ -511,7 +499,7 @@ public ChassisSpeeds getTargetSpeeds(double xInput, double yInput, Rotation2d an xInput = Math.pow(xInput, 3); yInput = Math.pow(yInput, 3); return swerveDrive.swerveController.getTargetSpeeds( - xInput, yInput, angle.getRadians(), getHeading().getRadians(), maximumSpeed); + xInput, yInput, angle.getRadians(), getRotation().getRadians(), maximumSpeed); } /** @@ -594,7 +582,8 @@ public SwerveModulePosition[] getModulePositions() { * measurements from vision less. This matrix is in the form [x, y, theta]^T, with units in meters * and radians. */ - public void updateVisionMeasurements( + @Override + public void addVisionMeasurement( Pose2d robotPose, double imageCaptureTime, Matrix stdVector) { swerveDrive.addVisionMeasurement(robotPose, imageCaptureTime, stdVector); } @@ -604,7 +593,8 @@ public void updateVisionMeasurements( * * @return The current pose of the simulated drivetrain as a {@link Pose2d}. */ - public Pose2d getMapleSimPose() { + @Override + public Pose2d getSimPose() { return swerveDrive.getMapleSimDrive().get().getSimulatedDriveTrainPose(); } @@ -646,4 +636,22 @@ public AngularVelocity getMaximumModuleAngleVelocity() { public void driveRobotRelative(ChassisSpeeds velocity) { swerveDrive.drive(velocity); } + + /** + * Gets a supplier of the current simulated drivetrain, or an empty optional if not running in + * simulation. + * + * @return A supplier of the current simulated drivetrain, or an empty optional if not running in + * simulation. + */ + @Override + public Supplier> getDriveTrainSimulationSupplier() { + return () -> + swerveDrive + .getMapleSimDrive() + .map( + obj -> + (AbstractDriveTrainSimulation) + (Object) swerveDrive.getMapleSimDrive().orElse(null)); + } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java index 57554705..155fc8be 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java @@ -7,12 +7,11 @@ package org.frc5010.common.drive.swerve.akit; +import static edu.wpi.first.units.Units.MetersPerSecond; +import static edu.wpi.first.units.Units.Second; import static edu.wpi.first.units.Units.Volts; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveBaseRadius; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.maxSpeedMetersPerSec; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.moduleTranslations; -import com.ctre.phoenix6.CANBus; +import com.pathplanner.lib.config.RobotConfig; import com.pathplanner.lib.util.DriveFeedforwards; import com.pathplanner.lib.util.PathPlannerLogging; import edu.wpi.first.hal.FRCNetComm.tInstances; @@ -22,6 +21,7 @@ import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.math.geometry.Twist2d; import edu.wpi.first.math.kinematics.ChassisSpeeds; import edu.wpi.first.math.kinematics.SwerveDriveKinematics; @@ -32,46 +32,44 @@ import edu.wpi.first.math.util.Units; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.units.measure.Force; +import edu.wpi.first.units.measure.Voltage; import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.smartdashboard.Field2d; import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.SubsystemBase; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.Constants; import frc.robot.Constants.Mode; -import frc.robot.generated.TunerConstants; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; -import org.frc5010.common.drive.pose.AkitSwervePose; +import java.util.function.Supplier; +import org.frc5010.common.commands.AkitDriveCommands; import org.frc5010.common.drive.pose.DrivePoseEstimator; +import org.frc5010.common.drive.pose.SwerveFunctionsPose; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; +import org.frc5010.common.drive.swerve.GenericSwerveDrivetrain; import org.frc5010.common.drive.swerve.GenericSwerveModuleInfo; import org.frc5010.common.drive.swerve.SwerveDriveFunctions; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; -import org.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; import org.littletonrobotics.junction.AutoLogOutput; import org.littletonrobotics.junction.Logger; +import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; public class AkitSwerveDrive extends SwerveDriveFunctions { - static final double ODOMETRY_FREQUENCY = - new CANBus(TunerConstants.DrivetrainConstants.CANBusName).isNetworkFD() ? 250.0 : 100.0; - - public static DriveTrainSimulationConfig mapleSimConfig = DriveTrainSimulationConfig.Default(); - static final Lock odometryLock = new ReentrantLock(); + private final AkitSwerveConfig config; private final GyroIO gyroIO; private final GyroIOInputsAutoLogged gyroInputs = new GyroIOInputsAutoLogged(); private final Module[] modules = new Module[4]; // FL, FR, BL, BR private SysIdRoutine sysId; private Field2d field = new Field2d(); // For visualization in SmartDashboard - public static SwerveDriveSimulation driveSimulation = null; + private Supplier robotConfigSupplier = () -> null; private final Alert gyroDisconnectedAlert = new Alert("Disconnected gyro, using kinematics as fallback.", AlertType.kError); - private SwerveDriveKinematics kinematics = new SwerveDriveKinematics(moduleTranslations); + private SwerveDriveKinematics kinematics; private Rotation2d rawGyroRotation = new Rotation2d(); private SwerveModulePosition[] lastModulePositions = // For delta tracking new SwerveModulePosition[] { @@ -80,30 +78,35 @@ public class AkitSwerveDrive extends SwerveDriveFunctions { new SwerveModulePosition(), new SwerveModulePosition() }; - private SwerveDrivePoseEstimator poseEstimator = - new SwerveDrivePoseEstimator(kinematics, rawGyroRotation, lastModulePositions, new Pose2d()); - + private SwerveDrivePoseEstimator poseEstimator; private final Consumer resetSimulationPoseCallBack; public AkitSwerveDrive( + AkitSwerveConfig config, GyroIO gyroIO, ModuleIO flModuleIO, ModuleIO frModuleIO, ModuleIO blModuleIO, ModuleIO brModuleIO, Consumer resetSimulationPoseCallBack) { + this.config = config; + kinematics = new SwerveDriveKinematics(getModuleTranslations()); + poseEstimator = + new SwerveDrivePoseEstimator( + kinematics, rawGyroRotation, lastModulePositions, new Pose2d()); + this.gyroIO = gyroIO; this.resetSimulationPoseCallBack = resetSimulationPoseCallBack; - modules[0] = new Module(flModuleIO, 0); - modules[1] = new Module(frModuleIO, 1); - modules[2] = new Module(blModuleIO, 2); - modules[3] = new Module(brModuleIO, 3); + modules[0] = new Module(flModuleIO, 0, config.FrontLeft); + modules[1] = new Module(frModuleIO, 1, config.FrontRight); + modules[2] = new Module(blModuleIO, 2, config.BackLeft); + modules[3] = new Module(brModuleIO, 3, config.BackRight); // Usage reporting for swerve template HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDriveSwerve_AdvantageKit); // Start odometry thread - SparkOdometryThread.getInstance().start(); + OdometryThread.getInstance().start(); // Should we keep this? // Pathfinding.setPathfinder(new LocalADStarAK()); @@ -119,6 +122,10 @@ public AkitSwerveDrive( }); } + public AkitSwerveConfig getConfig() { + return config; + } + @Override public void periodic() { odometryLock.lock(); // Prevents odometry updates while reading data @@ -142,6 +149,8 @@ public void periodic() { Logger.recordOutput("SwerveStates/SetpointsOptimized", new SwerveModuleState[] {}); } + getChassisSpeeds(); + // Update odometry double[] sampleTimestamps = modules[0].getOdometryTimestamps(); // All signals are sampled together @@ -187,7 +196,7 @@ public void runVelocity(ChassisSpeeds speeds) { // Calculate module setpoints ChassisSpeeds discreteSpeeds = ChassisSpeeds.discretize(speeds, 0.02); SwerveModuleState[] setpointStates = kinematics.toSwerveModuleStates(discreteSpeeds); - SwerveDriveKinematics.desaturateWheelSpeeds(setpointStates, maxSpeedMetersPerSec); + SwerveDriveKinematics.desaturateWheelSpeeds(setpointStates, config.getMaxDriveSpeed()); // Log unoptimized setpoints Logger.recordOutput("SwerveStates/Setpoints", setpointStates); @@ -205,7 +214,19 @@ public void runVelocity(ChassisSpeeds speeds) { /** Runs the drive in a straight line with the specified drive output. */ public void runCharacterization(double output) { for (int i = 0; i < 4; i++) { - modules[i].runCharacterization(output); + modules[i].runCharacterization(output, config); + } + } + + /** + * Runs the steer characterization routine on all modules. This command is used to measure the + * feedforward constants of the steer motors. + * + * @param output The output to send to the modules in volts. + */ + public void runSteerCharacterization(double output) { + for (int i = 0; i < 4; i++) { + modules[i].runSteerCharacterization(output); } } @@ -220,6 +241,7 @@ public void stop() { */ public void stopWithX() { Rotation2d[] headings = new Rotation2d[4]; + Translation2d[] moduleTranslations = getModuleTranslations(); for (int i = 0; i < 4; i++) { headings[i] = moduleTranslations[i].getAngle(); } @@ -246,6 +268,7 @@ private SwerveModuleState[] getModuleStates() { for (int i = 0; i < 4; i++) { states[i] = modules[i].getState(); } + Logger.recordOutput("SwerveStates/Measured", states); return states; } @@ -262,7 +285,9 @@ public SwerveModulePosition[] getModulePositions() { /** Returns the measured chassis speeds of the robot. */ @AutoLogOutput(key = "SwerveChassisSpeeds/Measured") private ChassisSpeeds getChassisSpeeds() { - return kinematics.toChassisSpeeds(getModuleStates()); + ChassisSpeeds speeds = kinematics.toChassisSpeeds(getModuleStates()); + Logger.recordOutput("SwerveChassisSpeeds/Measured", speeds); + return speeds; } /** Returns the position of each module in radians. */ @@ -275,10 +300,19 @@ public double[] getWheelRadiusCharacterizationPositions() { } /** Returns the average velocity of the modules in rad/sec. */ - public double getFFCharacterizationVelocity() { + public double getDriveFFCharacterizationVelocity() { + double output = 0.0; + for (int i = 0; i < 4; i++) { + output += modules[i].getDriveFFCharacterizationVelocity() / 4.0; + } + return output; + } + + /** Returns the average velocity of the modules in rad/sec. */ + public double getSteerFFCharacterizationVelocity() { double output = 0.0; for (int i = 0; i < 4; i++) { - output += modules[i].getFFCharacterizationVelocity() / 4.0; + output += modules[i].getSteerFFCharacterizationVelocity() / 4.0; } return output; } @@ -311,12 +345,12 @@ public void addVisionMeasurement( /** Returns the maximum linear speed in meters per sec. */ public double getMaxLinearSpeedMetersPerSec() { - return maxSpeedMetersPerSec; + return config.getMaxDriveSpeed().in(MetersPerSecond); } /** Returns the maximum angular speed in radians per sec. */ public double getMaxAngularSpeedRadPerSec() { - return maxSpeedMetersPerSec / driveBaseRadius; + return getMaxLinearSpeedMetersPerSec() / config.DRIVE_BASE_RADIUS; } @Override @@ -326,7 +360,7 @@ public Field2d getField2d() { @Override public DrivePoseEstimator initializePoseEstimator() { - return new DrivePoseEstimator(new AkitSwervePose(this)); + return new DrivePoseEstimator(new SwerveFunctionsPose(this)); } @Override @@ -336,7 +370,27 @@ public ChassisSpeeds getRobotVelocity() { @Override public ChassisSpeeds getFieldVelocity() { - return ChassisSpeeds.fromFieldRelativeSpeeds(getChassisSpeeds(), gyroInputs.yawPosition); + return ChassisSpeeds.fromRobotRelativeSpeeds(getChassisSpeeds(), getRotation()); + } + + /** + * Returns the robot-relative chassis acceleration derived from drive motor acceleration signals. + * Uses the same kinematics math as velocity, but with per-module acceleration instead of + * velocity. + */ + public ChassisSpeeds getChassisAcceleration() { + SwerveModuleState[] accelStates = new SwerveModuleState[4]; + for (int i = 0; i < 4; i++) { + accelStates[i] = modules[i].getAccelerationState(); + } + return kinematics.toChassisSpeeds(accelStates); + } + + /** + * Returns the field-relative chassis acceleration derived from drive motor acceleration signals. + */ + public ChassisSpeeds getFieldAcceleration() { + return ChassisSpeeds.fromRobotRelativeSpeeds(getChassisAcceleration(), getRotation()); } @Override @@ -362,14 +416,24 @@ public double getGyroRate() { return Units.radiansToDegrees(gyroInputs.yawVelocityRadPerSec); } + /** + * Returns a SysIdRoutine instance configured for system identification of the drive. + * + *

If the instance variable sysId is null, a new SysIdRoutine instance is created with the + * provided SubsystemBase and a default configuration. The default configuration includes a no-op + * mechanism and a logger which records the state of the SysIdRoutine to the {@link Logger}. + * + * @param swerveSubsystem The subsystem to add to the requirements of the SysIdRoutine + * @return A SysIdRoutine instance configured for system identification of the drive + */ protected SysIdRoutine getSysId(SubsystemBase swerveSubsystem) { if (null == sysId) { sysId = new SysIdRoutine( new SysIdRoutine.Config( - null, - null, - null, + Volts.of(0.5).per(Second), + Volts.of(7), + Second.of(30), (state) -> Logger.recordOutput("Drive/SysIdState", state.toString())), new SysIdRoutine.Mechanism( (voltage) -> runCharacterization(voltage.in(Volts)), null, swerveSubsystem)); @@ -381,9 +445,16 @@ protected SysIdRoutine getSysId(SubsystemBase swerveSubsystem) { public Command sysIdDriveMotorCommand(SubsystemBase swerveSubsystem) { // Configure SysId return sysIdQuasistatic(SysIdRoutine.Direction.kForward) + .withTimeout(10) + .andThen(Commands.waitSeconds(3)) .andThen(sysIdQuasistatic(SysIdRoutine.Direction.kReverse)) + .withTimeout(10) + .andThen(Commands.waitSeconds(3)) .andThen(sysIdDynamic(SysIdRoutine.Direction.kForward)) - .andThen(sysIdDynamic(SysIdRoutine.Direction.kReverse)); + .withTimeout(4) + .andThen(Commands.waitSeconds(3)) + .andThen(sysIdDynamic(SysIdRoutine.Direction.kReverse)) + .withTimeout(4); } @Override @@ -418,22 +489,66 @@ public GenericSwerveModuleInfo[] getModulesInfo() { return moduleInfos; } + /** Returns an array of module translations. */ + public Translation2d[] getModuleTranslations() { + return new Translation2d[] { + new Translation2d(config.FrontLeft.LocationX, config.FrontLeft.LocationY), + new Translation2d(config.FrontRight.LocationX, config.FrontRight.LocationY), + new Translation2d(config.BackLeft.LocationX, config.BackLeft.LocationY), + new Translation2d(config.BackRight.LocationX, config.BackRight.LocationY) + }; + } + /** * Retrieves the pose of the simulated drivetrain from the MapleSim system. * * @return The current pose of the simulated drivetrain as a {@link Pose2d}. */ - public Pose2d getMapleSimPose() { + @Override + public Pose2d getSimPose() { return driveSimulation.getSimulatedDriveTrainPose(); } + @Override + public Supplier getPPRobotConfigSupplier() { + return robotConfigSupplier; + } + + @Override + public void setPPRobotConfigSupplier(Supplier robotConfigSupplier) { + this.robotConfigSupplier = robotConfigSupplier; + } + public void updateSimulation() { SimulatedArena.getInstance().simulationPeriodic(); Logger.recordOutput( "FieldSimulation/RobotPosition", driveSimulation.getSimulatedDriveTrainPose()); Logger.recordOutput( - "FieldSimulation/Coral", SimulatedArena.getInstance().getGamePiecesArrayByType("Coral")); - Logger.recordOutput( - "FieldSimulation/Algae", SimulatedArena.getInstance().getGamePiecesArrayByType("Algae")); + "FieldSimulation/Fuel", SimulatedArena.getInstance().getGamePiecesArrayByType("Fuel")); + } + + @Override + public void addAutoCommands( + LoggedDashboardChooser selectableCommand, GenericSwerveDrivetrain drivetrain) { + selectableCommand.addOption( + "PRO: Swerve Wheel Radius Characterization", + AkitDriveCommands.wheelRadiusCharacterization(drivetrain, this)); + selectableCommand.addOption( + "PRO: Swerve Drive Feedforward Characterization", + AkitDriveCommands.feedforwardCharacterization( + drivetrain, + (Voltage voltage) -> runCharacterization(voltage.in(Volts)), + () -> getDriveFFCharacterizationVelocity())); + selectableCommand.addOption( + "PRO: Swerve Steer Feedforward Characterization", + AkitDriveCommands.feedforwardCharacterization( + drivetrain, + (Voltage voltage) -> runSteerCharacterization(voltage.in(Volts)), + () -> getSteerFFCharacterizationVelocity())); + + selectableCommand.addOption( + "PRO: Swerve Angle PID Tuning", AkitDriveCommands.steerPIDTuning(drivetrain, this)); + selectableCommand.addOption( + "PRO: Swerve Drive PID Tuning", AkitDriveCommands.drivePIDTuning(drivetrain, this)); } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIONavX.java b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIONavX.java index d8083208..a8666f3f 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIONavX.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIONavX.java @@ -22,8 +22,8 @@ public class GyroIONavX implements GyroIO { private final Queue yawTimestampQueue; public GyroIONavX() { - yawTimestampQueue = SparkOdometryThread.getInstance().makeTimestampQueue(); - yawPositionQueue = SparkOdometryThread.getInstance().registerSignal(navX::getAngle); + yawTimestampQueue = OdometryThread.getInstance().makeTimestampQueue(); + yawPositionQueue = OdometryThread.getInstance().registerSignal(navX::getAngle); } @Override diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java index c5f0ee86..fd311bd8 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java @@ -7,8 +7,6 @@ package org.frc5010.common.drive.swerve.akit; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.*; - import com.ctre.phoenix6.BaseStatusSignal; import com.ctre.phoenix6.StatusCode; import com.ctre.phoenix6.StatusSignal; @@ -18,33 +16,33 @@ import edu.wpi.first.math.util.Units; import edu.wpi.first.units.measure.Angle; import edu.wpi.first.units.measure.AngularVelocity; -import frc.robot.generated.TunerConstants; import java.util.Queue; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; /** IO implementation for Pigeon 2. */ public class GyroIOPigeon2 implements GyroIO { - private final Pigeon2 pigeon = new Pigeon2(TunerConstants.kPigeonId); - private final StatusSignal yaw = pigeon.getYaw(); + private final Pigeon2 pigeon; + private final StatusSignal yaw; private final Queue yawPositionQueue; private final Queue yawTimestampQueue; - private final StatusSignal yawVelocity = pigeon.getAngularVelocityZWorld(); + private final StatusSignal yawVelocity; - public GyroIOPigeon2() { + public GyroIOPigeon2(AkitSwerveConfig config) { + pigeon = new Pigeon2(config.DrivetrainConstants.Pigeon2Id, config.getCANBus()); + yaw = pigeon.getYaw(); + yawVelocity = pigeon.getAngularVelocityZWorld(); pigeon.getConfigurator().apply(new Pigeon2Configuration()); pigeon.getConfigurator().setYaw(0.0); - yaw.setUpdateFrequency(odometryFrequency); + yaw.setUpdateFrequency(config.ODOMETRY_FREQUENCY); yawVelocity.setUpdateFrequency(50.0); pigeon.optimizeBusUtilization(); - yawTimestampQueue = SparkOdometryThread.getInstance().makeTimestampQueue(); - var yawClone = yaw.clone(); // Status signals are not thread-safe - yawPositionQueue = - SparkOdometryThread.getInstance() - .registerSignal(() -> yawClone.refresh().getValueAsDouble()); + yawTimestampQueue = TalonFXOdometryThread.getInstance().makeTimestampQueue(); + yawPositionQueue = TalonFXOdometryThread.getInstance().registerSignal(yaw.clone()); } @Override public void updateInputs(GyroIOInputs inputs) { - inputs.connected = BaseStatusSignal.refreshAll(yaw, yawVelocity).equals(StatusCode.OK); + inputs.connected = BaseStatusSignal.refreshAll(yawVelocity).equals(StatusCode.OK); inputs.yawPosition = Rotation2d.fromDegrees(yaw.getValueAsDouble()); inputs.yawVelocityRadPerSec = Units.degreesToRadians(yawVelocity.getValueAsDouble()); diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOSim.java b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOSim.java index 50db321f..a74c3a27 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOSim.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOSim.java @@ -4,7 +4,7 @@ import edu.wpi.first.math.util.Units; import org.frc5010.common.drive.swerve.akit.util.PhoenixUtil; -import org.ironmaple.simulation.drivesims.GyroSimulation; +import swervelib.simulation.ironmaple.simulation.drivesims.GyroSimulation; public class GyroIOSim implements GyroIO { private final GyroSimulation gyroSimulation; diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java b/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java index bb9fa18a..b911d5c1 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java @@ -7,27 +7,40 @@ package org.frc5010.common.drive.swerve.akit; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.*; - +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.kinematics.SwerveModulePosition; import edu.wpi.first.math.kinematics.SwerveModuleState; import edu.wpi.first.wpilibj.Alert; import edu.wpi.first.wpilibj.Alert.AlertType; +import frc.robot.Robot; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; import org.littletonrobotics.junction.Logger; public class Module { private final ModuleIO io; private final ModuleIOInputsAutoLogged inputs = new ModuleIOInputsAutoLogged(); private final int index; + private final SwerveModuleConstants< + TalonFXConfiguration, TalonFXConfiguration, CANcoderConfiguration> + constants; private final Alert driveDisconnectedAlert; private final Alert turnDisconnectedAlert; + private final Alert turnEncoderDisconnectedAlert; + private SwerveModulePosition[] odometryPositions = new SwerveModulePosition[] {}; - public Module(ModuleIO io, int index) { + public Module( + ModuleIO io, + int index, + SwerveModuleConstants + constants) { this.io = io; this.index = index; + this.constants = constants; driveDisconnectedAlert = new Alert( "Disconnected drive motor on module " + Integer.toString(index) + ".", @@ -35,6 +48,10 @@ public Module(ModuleIO io, int index) { turnDisconnectedAlert = new Alert( "Disconnected turn motor on module " + Integer.toString(index) + ".", AlertType.kError); + turnEncoderDisconnectedAlert = + new Alert( + "Disconnected turn encoder on module " + Integer.toString(index) + ".", + AlertType.kError); } public void periodic() { @@ -48,8 +65,7 @@ public void periodic() { inputs.odometryTurnPositions.length); // All signals are sampled together odometryPositions = new SwerveModulePosition[sampleCount]; for (int i = 0; i < sampleCount; i++) { - double positionMeters = inputs.odometryDrivePositionsRad[i] * wheelRadiusMeters; - + double positionMeters = inputs.odometryDrivePositionsRad[i] * constants.WheelRadius; Rotation2d angle = inputs.odometryTurnPositions[i]; odometryPositions[i] = new SwerveModulePosition(positionMeters, angle); } @@ -57,23 +73,35 @@ public void periodic() { // Update alerts driveDisconnectedAlert.set(!inputs.driveConnected); turnDisconnectedAlert.set(!inputs.turnConnected); + turnEncoderDisconnectedAlert.set(!inputs.turnEncoderConnected); } /** Runs the module with the specified setpoint state. Mutates the state to optimize it. */ public void runSetpoint(SwerveModuleState state) { // Optimize velocity setpoint state.optimize(getAngle()); - state.cosineScale(inputs.turnAbsolutePosition); + state.cosineScale(Robot.isSimulation() ? inputs.turnAbsolutePosition : inputs.turnPosition); // Apply setpoints - io.setDriveVelocity(state.speedMetersPerSecond / wheelRadiusMeters); + io.setDriveVelocity(state.speedMetersPerSecond / constants.WheelRadius); io.setTurnPosition(state.angle); } - /** Runs the module with the specified output while controlling to zero degrees. */ - public void runCharacterization(double output) { + /** Runs the module with the specified output while controlling to rotation angles. */ + public void runCharacterization(double output, AkitSwerveConfig config) { io.setDriveOpenLoop(output); - io.setTurnPosition(new Rotation2d()); + // CLR - This sets the turn positions to rotate the robot + io.setTurnPosition( + new Rotation2d( + config.getModuleConstants(index).LocationX, + config.getModuleConstants(index).LocationY) + .plus(Rotation2d.kCCW_Pi_2)); + } + + /** Runs the module with the specified output while controlling to zero degrees. */ + public void runSteerCharacterization(double output) { + io.setDriveOpenLoop(0); + io.setTurnOpenLoop(output); } /** Disables all outputs to motors. */ @@ -84,17 +112,20 @@ public void stop() { /** Returns the current turn angle of the module. */ public Rotation2d getAngle() { - return inputs.turnAbsolutePosition; + if (Robot.isSimulation()) { + return inputs.turnAbsolutePosition; + } + return inputs.turnPosition; } /** Returns the current drive position of the module in meters. */ public double getPositionMeters() { - return inputs.drivePositionRad * wheelRadiusMeters; + return inputs.drivePositionRad * constants.WheelRadius; } /** Returns the current drive velocity of the module in meters per second. */ public double getVelocityMetersPerSec() { - return inputs.driveVelocityRadPerSec * wheelRadiusMeters; + return inputs.driveVelocityRadPerSec * constants.WheelRadius; } /** Returns the module position (turn angle and drive position). */ @@ -122,8 +153,23 @@ public double getWheelRadiusCharacterizationPosition() { return inputs.drivePositionRad; } - /** Returns the module velocity in rad/sec. */ - public double getFFCharacterizationVelocity() { + /** Returns the module drive velocity in rad/sec. */ + public double getDriveFFCharacterizationVelocity() { return inputs.driveVelocityRadPerSec; } + + /** Returns the module drive acceleration in m/s², converted from rad/s² using wheel radius. */ + public double getDriveAccelerationMetersPerSecSquared() { + return inputs.driveAccelerationRadPerSecSquared * constants.WheelRadius; + } + + /** Returns the current drive acceleration state (velocity and heading) for chassis kinematics. */ + public SwerveModuleState getAccelerationState() { + return new SwerveModuleState(getDriveAccelerationMetersPerSecSquared(), getAngle()); + } + + /** Returns the module steer velocity in rad/sec. */ + public double getSteerFFCharacterizationVelocity() { + return inputs.turnVelocityRadPerSec; + } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java index c96767ac..d4199b10 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java @@ -16,12 +16,14 @@ class ModuleIOInputs { public boolean driveConnected = false; public double drivePositionRad = 0.0; public double driveVelocityRadPerSec = 0.0; + public double driveAccelerationRadPerSecSquared = 0.0; public double driveAppliedVolts = 0.0; public double driveCurrentAmps = 0.0; public boolean turnConnected = false; public boolean turnEncoderConnected = false; - public Rotation2d turnAbsolutePosition = new Rotation2d(); + public Rotation2d turnPosition = Rotation2d.kZero; + public Rotation2d turnAbsolutePosition = Rotation2d.kZero; public double turnVelocityRadPerSec = 0.0; public double turnAppliedVolts = 0.0; public double turnCurrentAmps = 0.0; diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSim.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSim.java new file mode 100644 index 00000000..fd76a9fc --- /dev/null +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSim.java @@ -0,0 +1,112 @@ +// Copyright (c) 2021-2025 Littleton Robotics +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by a BSD +// license that can be found in the LICENSE file +// at the root directory of this project. + +package org.frc5010.common.drive.swerve.akit; + +import static org.frc5010.common.drive.swerve.akit.DriveConstants.*; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.controller.PIDController; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.system.plant.LinearSystemId; +import edu.wpi.first.wpilibj.Timer; +import edu.wpi.first.wpilibj.simulation.DCMotorSim; + +/** Physics sim implementation of module IO. */ +public class ModuleIOSim implements ModuleIO { + private final DCMotorSim driveSim; + private final DCMotorSim turnSim; + + private boolean driveClosedLoop = false; + private boolean turnClosedLoop = false; + private PIDController driveController = new PIDController(driveSimP, 0, driveSimD); + private PIDController turnController = new PIDController(turnSimP, 0, turnSimD); + private double driveFFVolts = 0.0; + private double driveAppliedVolts = 0.0; + private double turnAppliedVolts = 0.0; + + public ModuleIOSim() { + // Create drive and turn sim models + driveSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem(driveGearbox, 0.025, driveMotorReduction), + driveGearbox); + turnSim = + new DCMotorSim( + LinearSystemId.createDCMotorSystem(turnGearbox, 0.004, turnMotorReduction), + turnGearbox); + + // Enable wrapping for turn PID + turnController.enableContinuousInput(-Math.PI, Math.PI); + } + + @Override + public void updateInputs(ModuleIOInputs inputs) { + // Run closed-loop control + if (driveClosedLoop) { + driveAppliedVolts = + driveFFVolts + driveController.calculate(driveSim.getAngularVelocityRadPerSec()); + } else { + driveController.reset(); + } + if (turnClosedLoop) { + turnAppliedVolts = turnController.calculate(turnSim.getAngularPositionRad()); + } else { + turnController.reset(); + } + + // Update simulation state + driveSim.setInputVoltage(MathUtil.clamp(driveAppliedVolts, -12.0, 12.0)); + turnSim.setInputVoltage(MathUtil.clamp(turnAppliedVolts, -12.0, 12.0)); + driveSim.update(0.02); + turnSim.update(0.02); + + // Update drive inputs + inputs.driveConnected = true; + inputs.drivePositionRad = driveSim.getAngularPositionRad(); + inputs.driveVelocityRadPerSec = driveSim.getAngularVelocityRadPerSec(); + inputs.driveAppliedVolts = driveAppliedVolts; + inputs.driveCurrentAmps = Math.abs(driveSim.getCurrentDrawAmps()); + + // Update turn inputs + inputs.turnConnected = true; + inputs.turnAbsolutePosition = new Rotation2d(turnSim.getAngularPositionRad()); + inputs.turnVelocityRadPerSec = turnSim.getAngularVelocityRadPerSec(); + inputs.turnAppliedVolts = turnAppliedVolts; + inputs.turnCurrentAmps = Math.abs(turnSim.getCurrentDrawAmps()); + + // Update odometry inputs (50Hz because high-frequency odometry in sim doesn't matter) + inputs.odometryTimestamps = new double[] {Timer.getFPGATimestamp()}; + inputs.odometryDrivePositionsRad = new double[] {inputs.drivePositionRad}; + inputs.odometryTurnPositions = new Rotation2d[] {inputs.turnAbsolutePosition}; + } + + @Override + public void setDriveOpenLoop(double output) { + driveClosedLoop = false; + driveAppliedVolts = output; + } + + @Override + public void setTurnOpenLoop(double output) { + turnClosedLoop = false; + turnAppliedVolts = output; + } + + @Override + public void setDriveVelocity(double velocityRadPerSec) { + driveClosedLoop = true; + driveFFVolts = driveSimKs * Math.signum(velocityRadPerSec) + driveSimKv * velocityRadPerSec; + driveController.setSetpoint(velocityRadPerSec); + } + + @Override + public void setTurnPosition(Rotation2d rotation) { + turnClosedLoop = true; + turnController.setSetpoint(rotation.getRadians()); + } +} diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java index 44034d22..8e6f652e 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java @@ -7,22 +7,52 @@ package org.frc5010.common.drive.swerve.akit; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.*; -import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.*; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.backLeftDriveCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.backLeftTurnCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.backLeftZeroRotation; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.backRightDriveCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.backRightTurnCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.backRightZeroRotation; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveEncoderPositionFactor; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveEncoderVelocityFactor; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKd; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKp; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKs; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKv; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveMotorCurrentLimit; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontLeftDriveCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontLeftTurnCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontLeftZeroRotation; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontRightDriveCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontRightTurnCanId; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontRightZeroRotation; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.odometryFrequency; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderInverted; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderPositionFactor; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderVelocityFactor; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnInverted; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKd; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKp; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnMotorCurrentLimit; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMaxInput; +import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMinInput; +import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.ifOk; +import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.sparkStickyFault; +import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.tryUntilOk; import com.revrobotics.AbsoluteEncoder; +import com.revrobotics.PersistMode; import com.revrobotics.RelativeEncoder; +import com.revrobotics.ResetMode; import com.revrobotics.spark.ClosedLoopSlot; +import com.revrobotics.spark.FeedbackSensor; import com.revrobotics.spark.SparkBase; import com.revrobotics.spark.SparkBase.ControlType; -import com.revrobotics.spark.SparkBase.PersistMode; -import com.revrobotics.spark.SparkBase.ResetMode; import com.revrobotics.spark.SparkClosedLoopController; import com.revrobotics.spark.SparkClosedLoopController.ArbFFUnits; import com.revrobotics.spark.SparkFlex; import com.revrobotics.spark.SparkLowLevel.MotorType; import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; import com.revrobotics.spark.config.SparkFlexConfig; import com.revrobotics.spark.config.SparkMaxConfig; @@ -104,12 +134,7 @@ public ModuleIOSpark(int module) { .velocityConversionFactor(driveEncoderVelocityFactor) .uvwMeasurementPeriod(10) .uvwAverageDepth(2); - driveConfig - .closedLoop - .feedbackSensor(FeedbackSensor.kPrimaryEncoder) - .pidf( - driveKp, 0.0, - driveKd, 0.0); + driveConfig.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder).p(driveKp).d(driveKd); driveConfig .signals .primaryEncoderPositionAlwaysOn(true) @@ -145,7 +170,8 @@ public ModuleIOSpark(int module) { .feedbackSensor(FeedbackSensor.kAbsoluteEncoder) .positionWrappingEnabled(true) .positionWrappingInputRange(turnPIDMinInput, turnPIDMaxInput) - .pidf(turnKp, 0.0, turnKd, 0.0); + .p(turnKp) + .d(turnKd); turnConfig .signals .absoluteEncoderPositionAlwaysOn(true) @@ -224,7 +250,7 @@ public void setTurnOpenLoop(double output) { @Override public void setDriveVelocity(double velocityRadPerSec) { double ffVolts = driveKs * Math.signum(velocityRadPerSec) + driveKv * velocityRadPerSec; - driveController.setReference( + driveController.setSetpoint( velocityRadPerSec, ControlType.kVelocity, ClosedLoopSlot.kSlot0, @@ -237,6 +263,6 @@ public void setTurnPosition(Rotation2d rotation) { double setpoint = MathUtil.inputModulus( rotation.plus(zeroRotation).getRadians(), turnPIDMinInput, turnPIDMaxInput); - turnController.setReference(setpoint, ControlType.kPosition); + turnController.setSetpoint(setpoint, ControlType.kPosition); } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java index d3ff9850..32f914ea 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java @@ -19,7 +19,6 @@ import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMinInput; import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.ifOk; import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.sparkStickyFault; -import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.tryUntilOk; import com.ctre.phoenix6.BaseStatusSignal; import com.ctre.phoenix6.StatusSignal; @@ -36,14 +35,12 @@ import com.ctre.phoenix6.signals.NeutralModeValue; import com.ctre.phoenix6.swerve.SwerveModuleConstants; import com.revrobotics.AbsoluteEncoder; +import com.revrobotics.spark.FeedbackSensor; import com.revrobotics.spark.SparkBase; import com.revrobotics.spark.SparkBase.ControlType; -import com.revrobotics.spark.SparkBase.PersistMode; -import com.revrobotics.spark.SparkBase.ResetMode; import com.revrobotics.spark.SparkClosedLoopController; import com.revrobotics.spark.SparkLowLevel.MotorType; import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; import com.revrobotics.spark.config.SparkMaxConfig; import edu.wpi.first.math.MathUtil; @@ -54,9 +51,9 @@ import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.units.measure.Current; import edu.wpi.first.units.measure.Voltage; -import frc.robot.generated.TunerConstants; import java.util.Queue; import java.util.function.DoubleSupplier; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; import org.frc5010.common.drive.swerve.akit.util.PhoenixUtil; /** @@ -104,12 +101,13 @@ public class ModuleIOSparkTalon implements ModuleIO { private final Debouncer turnConnectedDebounce = new Debouncer(0.5); public ModuleIOSparkTalon( + AkitSwerveConfig config, SwerveModuleConstants constants) { this.constants = constants; zeroRotation = new Rotation2d(); - driveTalon = new TalonFX(constants.DriveMotorId, TunerConstants.DrivetrainConstants.CANBusName); + driveTalon = new TalonFX(constants.DriveMotorId, config.getCANBus()); turnSpark = new SparkMax(constants.SteerMotorId, MotorType.kBrushless); turnEncoder = turnSpark.getAbsoluteEncoder(); @@ -151,7 +149,8 @@ public ModuleIOSparkTalon( .feedbackSensor(FeedbackSensor.kAbsoluteEncoder) .positionWrappingEnabled(true) .positionWrappingInputRange(turnPIDMinInput, turnPIDMaxInput) - .pidf(turnKp, 0.0, turnKd, 0.0); + .p(turnKp) + .d(turnKd); turnConfig .signals .absoluteEncoderPositionAlwaysOn(true) @@ -161,16 +160,10 @@ public ModuleIOSparkTalon( .appliedOutputPeriodMs(20) .busVoltagePeriodMs(20) .outputCurrentPeriodMs(20); - tryUntilOk( - turnSpark, - 5, - () -> - turnSpark.configure( - turnConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters)); // Create drive status signals drivePosition = driveTalon.getPosition(); - drivePositionQueue = PhoenixOdometryThread.getInstance().registerSignal(drivePosition.clone()); + drivePositionQueue = TalonFXOdometryThread.getInstance().registerSignal(drivePosition.clone()); driveVelocity = driveTalon.getVelocity(); driveAppliedVolts = driveTalon.getMotorVoltage(); driveCurrent = driveTalon.getStatorCurrent(); @@ -252,6 +245,6 @@ public void setTurnPosition(Rotation2d rotation) { double setpoint = MathUtil.inputModulus( rotation.plus(zeroRotation).getRadians(), turnPIDMinInput, turnPIDMaxInput); - turnController.setReference(setpoint, ControlType.kPosition); + turnController.setSetpoint(setpoint, ControlType.kPosition); } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFX.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFX.java index 62e75cbd..19218257 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFX.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFX.java @@ -24,11 +24,12 @@ import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.util.Units; import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularAcceleration; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.units.measure.Current; import edu.wpi.first.units.measure.Voltage; import frc.robot.Constants; -import frc.robot.generated.TunerConstants; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; public abstract class ModuleIOTalonFX implements ModuleIO { protected final SwerveModuleConstants< @@ -53,10 +54,12 @@ public abstract class ModuleIOTalonFX implements ModuleIO { // Inputs from drive motor protected final StatusSignal drivePosition; protected final StatusSignal driveVelocity; + protected final StatusSignal driveAcceleration; protected final StatusSignal driveAppliedVolts; protected final StatusSignal driveCurrent; // Inputs from turn motor + protected final StatusSignal turnPosition; protected final StatusSignal turnAbsolutePosition; protected final StatusSignal turnVelocity; protected final StatusSignal turnAppliedVolts; @@ -68,18 +71,20 @@ public abstract class ModuleIOTalonFX implements ModuleIO { private final Debouncer turnEncoderConnectedDebounce = new Debouncer(0.5); protected ModuleIOTalonFX( + AkitSwerveConfig config, SwerveModuleConstants constants) { this.constants = constants; - driveTalon = new TalonFX(constants.DriveMotorId, TunerConstants.DrivetrainConstants.CANBusName); - turnTalon = new TalonFX(constants.SteerMotorId, TunerConstants.DrivetrainConstants.CANBusName); - cancoder = new CANcoder(constants.EncoderId, TunerConstants.DrivetrainConstants.CANBusName); + driveTalon = new TalonFX(constants.DriveMotorId, config.getCANBus()); + turnTalon = new TalonFX(constants.SteerMotorId, config.getCANBus()); + cancoder = new CANcoder(constants.EncoderId, config.getCANBus()); // Configure drive motor var driveConfig = constants.DriveMotorInitialConfigs; driveConfig.MotorOutput.NeutralMode = NeutralModeValue.Brake; driveConfig.Slot0 = constants.DriveMotorGains; + driveConfig.Feedback.SensorToMechanismRatio = constants.DriveMotorGearRatio; driveConfig.TorqueCurrent.PeakForwardTorqueCurrent = constants.SlipCurrent; driveConfig.TorqueCurrent.PeakReverseTorqueCurrent = -constants.SlipCurrent; driveConfig.CurrentLimits.StatorCurrentLimit = constants.SlipCurrent; @@ -133,10 +138,12 @@ protected ModuleIOTalonFX( // Create drive status signals drivePosition = driveTalon.getPosition(); driveVelocity = driveTalon.getVelocity(); + driveAcceleration = driveTalon.getAcceleration(); driveAppliedVolts = driveTalon.getMotorVoltage(); driveCurrent = driveTalon.getStatorCurrent(); // Create turn status signals + turnPosition = turnTalon.getPosition(); turnAbsolutePosition = cancoder.getAbsolutePosition(); turnVelocity = turnTalon.getVelocity(); turnAppliedVolts = turnTalon.getMotorVoltage(); @@ -144,36 +151,44 @@ protected ModuleIOTalonFX( // Configure periodic frames BaseStatusSignal.setUpdateFrequencyForAll( - AkitSwerveDrive.ODOMETRY_FREQUENCY, turnAbsolutePosition, drivePosition); + config.ODOMETRY_FREQUENCY, turnPosition, drivePosition, turnAbsolutePosition); BaseStatusSignal.setUpdateFrequencyForAll( 50.0, driveVelocity, + driveAcceleration, driveAppliedVolts, driveCurrent, turnVelocity, turnAppliedVolts, turnCurrent); - ParentDevice.optimizeBusUtilizationForAll(driveTalon, turnTalon); + ParentDevice.optimizeBusUtilizationForAll(driveTalon, turnTalon, cancoder); } @Override public void updateInputs(ModuleIOInputs inputs) { // Refresh all signals var driveStatus = - BaseStatusSignal.refreshAll(drivePosition, driveVelocity, driveAppliedVolts, driveCurrent); - var turnStatus = BaseStatusSignal.refreshAll(turnVelocity, turnAppliedVolts, turnCurrent); - var turnEncoderStatus = BaseStatusSignal.refreshAll(turnAbsolutePosition); + BaseStatusSignal.refreshAll( + driveVelocity, driveAcceleration, driveAppliedVolts, driveCurrent); + // Refresh turnAbsolutePosition alongside turn motor signals so it is always up-to-date. + // In simulation the odometry thread does not run, so signals registered only there would + // stay permanently at 0 — causing tank-drive behavior and stuck module angles in sim. + var turnStatus = + BaseStatusSignal.refreshAll( + turnAbsolutePosition, turnVelocity, turnAppliedVolts, turnCurrent); + var turnEncoderStatus = turnStatus; // turnAbsolutePosition already refreshed above // Update drive inputs inputs.driveConnected = driveConnectedDebounce.calculate(driveStatus.isOK()); - inputs.drivePositionRad = - Units.rotationsToRadians(drivePosition.getValueAsDouble()) / constants.DriveMotorGearRatio; - inputs.driveVelocityRadPerSec = - Units.rotationsToRadians(driveVelocity.getValueAsDouble()) / constants.DriveMotorGearRatio; + inputs.drivePositionRad = Units.rotationsToRadians(drivePosition.getValueAsDouble()); + inputs.driveVelocityRadPerSec = Units.rotationsToRadians(driveVelocity.getValueAsDouble()); + inputs.driveAccelerationRadPerSecSquared = + Units.rotationsToRadians(driveAcceleration.getValueAsDouble()); inputs.driveAppliedVolts = driveAppliedVolts.getValueAsDouble(); inputs.driveCurrentAmps = driveCurrent.getValueAsDouble(); // Update turn inputs + inputs.turnPosition = Rotation2d.fromRotations(turnAbsolutePosition.getValueAsDouble()); inputs.turnConnected = turnConnectedDebounce.calculate(turnStatus.isOK()); inputs.turnEncoderConnected = turnEncoderConnectedDebounce.calculate(turnEncoderStatus.isOK()); inputs.turnAbsolutePosition = Rotation2d.fromRotations(turnAbsolutePosition.getValueAsDouble()); @@ -202,13 +217,11 @@ public void setTurnOpenLoop(double output) { @Override public void setDriveVelocity(double wheelVelocityRadPerSec) { - double motorVelocityRotPerSec = - Units.radiansToRotations(wheelVelocityRadPerSec) * constants.DriveMotorGearRatio; + double velocityRotPerSec = Units.radiansToRotations(wheelVelocityRadPerSec); driveTalon.setControl( switch (constants.DriveMotorClosedLoopOutput) { - case Voltage -> velocityVoltageRequest.withVelocity(motorVelocityRotPerSec); - case TorqueCurrentFOC -> velocityTorqueCurrentRequest.withVelocity( - motorVelocityRotPerSec); + case Voltage -> velocityVoltageRequest.withVelocity(velocityRotPerSec); + case TorqueCurrentFOC -> velocityTorqueCurrentRequest.withVelocity(velocityRotPerSec); }); } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXReal.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXReal.java index 2e43674b..278d410f 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXReal.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXReal.java @@ -17,6 +17,7 @@ import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.util.Units; import java.util.Queue; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; /** * Module IO implementation for Talon FX drive motor controller, Talon FX turn motor controller, and @@ -30,14 +31,14 @@ public class ModuleIOTalonFXReal extends ModuleIOTalonFX { private final Queue drivePositionQueue; private final Queue turnPositionQueue; - public ModuleIOTalonFXReal(SwerveModuleConstants constants) { - super(constants); + public ModuleIOTalonFXReal(AkitSwerveConfig config, SwerveModuleConstants constants) { + super(config, constants); - this.timestampQueue = PhoenixOdometryThread.getInstance().makeTimestampQueue(); + this.timestampQueue = TalonFXOdometryThread.getInstance().makeTimestampQueue(); this.drivePositionQueue = - PhoenixOdometryThread.getInstance().registerSignal(super.drivePosition); + TalonFXOdometryThread.getInstance().registerSignal(super.drivePosition); this.turnPositionQueue = - PhoenixOdometryThread.getInstance().registerSignal(super.turnAbsolutePosition); + TalonFXOdometryThread.getInstance().registerSignal(super.turnAbsolutePosition); } @Override diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXSim.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXSim.java index 0c19c320..d1619fd4 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXSim.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOTalonFXSim.java @@ -17,8 +17,9 @@ import com.ctre.phoenix6.swerve.SwerveModuleConstants; import java.util.Arrays; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; import org.frc5010.common.drive.swerve.akit.util.PhoenixUtil; -import org.ironmaple.simulation.drivesims.SwerveModuleSimulation; +import swervelib.simulation.ironmaple.simulation.drivesims.SwerveModuleSimulation; /** * Physics sim implementation of module IO. The sim models are configured using a set of module @@ -27,8 +28,9 @@ public class ModuleIOTalonFXSim extends ModuleIOTalonFX { private final SwerveModuleSimulation simulation; - public ModuleIOTalonFXSim(SwerveModuleConstants constants, SwerveModuleSimulation simulation) { - super(PhoenixUtil.regulateModuleConstantForSimulation(constants)); + public ModuleIOTalonFXSim( + AkitSwerveConfig config, SwerveModuleConstants constants, SwerveModuleSimulation simulation) { + super(config, PhoenixUtil.regulateModuleConstantForSimulation(constants)); this.simulation = simulation; simulation.useDriveMotorController(new PhoenixUtil.TalonFXMotorControllerSim(driveTalon)); diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/OdometryThread.java b/src/main/java/org/frc5010/common/drive/swerve/akit/OdometryThread.java new file mode 100644 index 00000000..cd00fe06 --- /dev/null +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/OdometryThread.java @@ -0,0 +1,80 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.drive.swerve.akit; + +import edu.wpi.first.wpilibj.RobotBase; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.DoubleSupplier; +import org.frc5010.common.drive.swerve.SwerveDriveFunctions; + +/** + * Abstract base class for odometry threads, providing common functionality for managing signals, + * queues, and periodic updates. + */ +public abstract class OdometryThread extends Thread { + protected final Lock signalsLock = new ReentrantLock(); + protected final List genericSignals = new ArrayList<>(); + protected final List> genericQueues = new ArrayList<>(); + protected final List> timestampQueues = new ArrayList<>(); + protected static OdometryThread commonInstance; + + public static OdometryThread getInstance() { + return commonInstance; + } + /** Creates a new odometry thread with the specified name. */ + protected OdometryThread(String threadName) { + setName(threadName); + setDaemon(true); + } + + @Override + public void start() { + if (!timestampQueues.isEmpty() && RobotBase.isReal()) { + super.start(); + } + } + + /** Registers a generic signal to be read from the thread. */ + public Queue registerSignal(DoubleSupplier signal) { + Queue queue = new ArrayBlockingQueue<>(20); + signalsLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); + try { + genericSignals.add(signal); + genericQueues.add(queue); + } finally { + signalsLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); + } + return queue; + } + + /** Returns a new queue that returns timestamp values for each sample. */ + public Queue makeTimestampQueue() { + Queue queue = new ArrayBlockingQueue<>(20); + SwerveDriveFunctions.odometryLock.lock(); + try { + timestampQueues.add(queue); + } finally { + SwerveDriveFunctions.odometryLock.unlock(); + } + return queue; + } + + /** Abstract method to be implemented by subclasses for specific signal handling. */ + protected abstract void runThreadLogic(); + + @Override + public void run() { + while (true) { + runThreadLogic(); + } + } +} diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/PhoenixOdometryThread.java b/src/main/java/org/frc5010/common/drive/swerve/akit/PhoenixOdometryThread.java index 94dcc1cb..c1a0b938 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/PhoenixOdometryThread.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/PhoenixOdometryThread.java @@ -8,11 +8,10 @@ package org.frc5010.common.drive.swerve.akit; import com.ctre.phoenix6.BaseStatusSignal; -import com.ctre.phoenix6.CANBus; import com.ctre.phoenix6.StatusSignal; import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.RobotController; -import frc.robot.generated.TunerConstants; import java.util.ArrayList; import java.util.List; import java.util.Queue; @@ -20,6 +19,8 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.DoubleSupplier; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; +import org.frc5010.common.drive.swerve.SwerveDriveFunctions; /** * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. @@ -37,26 +38,31 @@ public class PhoenixOdometryThread extends Thread { private final List> phoenixQueues = new ArrayList<>(); private final List> genericQueues = new ArrayList<>(); private final List> timestampQueues = new ArrayList<>(); + AkitSwerveConfig config; - private static boolean isCANFD = - new CANBus(TunerConstants.DrivetrainConstants.CANBusName).isNetworkFD(); + private static boolean isCANFD; private static PhoenixOdometryThread instance = null; public static PhoenixOdometryThread getInstance() { + return instance; + } + + public static void createInstance(AkitSwerveConfig config) { if (instance == null) { - instance = new PhoenixOdometryThread(); + instance = new PhoenixOdometryThread(config); } - return instance; } - private PhoenixOdometryThread() { + private PhoenixOdometryThread(AkitSwerveConfig config) { + this.config = config; + isCANFD = config.getCANBus().isNetworkFD(); setName("PhoenixOdometryThread"); setDaemon(true); } @Override public void start() { - if (timestampQueues.size() > 0) { + if (!timestampQueues.isEmpty() && RobotBase.isReal()) { super.start(); } } @@ -65,7 +71,7 @@ public void start() { public Queue registerSignal(StatusSignal signal) { Queue queue = new ArrayBlockingQueue<>(20); signalsLock.lock(); - AkitSwerveDrive.odometryLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); try { BaseStatusSignal[] newSignals = new BaseStatusSignal[phoenixSignals.length + 1]; System.arraycopy(phoenixSignals, 0, newSignals, 0, phoenixSignals.length); @@ -74,7 +80,7 @@ public Queue registerSignal(StatusSignal signal) { phoenixQueues.add(queue); } finally { signalsLock.unlock(); - AkitSwerveDrive.odometryLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); } return queue; } @@ -83,13 +89,13 @@ public Queue registerSignal(StatusSignal signal) { public Queue registerSignal(DoubleSupplier signal) { Queue queue = new ArrayBlockingQueue<>(20); signalsLock.lock(); - AkitSwerveDrive.odometryLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); try { genericSignals.add(signal); genericQueues.add(queue); } finally { signalsLock.unlock(); - AkitSwerveDrive.odometryLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); } return queue; } @@ -97,11 +103,11 @@ public Queue registerSignal(DoubleSupplier signal) { /** Returns a new queue that returns timestamp values for each sample. */ public Queue makeTimestampQueue() { Queue queue = new ArrayBlockingQueue<>(20); - AkitSwerveDrive.odometryLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); try { timestampQueues.add(queue); } finally { - AkitSwerveDrive.odometryLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); } return queue; } @@ -113,12 +119,12 @@ public void run() { signalsLock.lock(); try { if (isCANFD && phoenixSignals.length > 0) { - BaseStatusSignal.waitForAll(2.0 / AkitSwerveDrive.ODOMETRY_FREQUENCY, phoenixSignals); + BaseStatusSignal.waitForAll(2.0 / config.ODOMETRY_FREQUENCY, phoenixSignals); } else { // "waitForAll" does not support blocking on multiple signals with a bus // that is not CAN FD, regardless of Pro licensing. No reasoning for this // behavior is provided by the documentation. - Thread.sleep((long) (1000.0 / AkitSwerveDrive.ODOMETRY_FREQUENCY)); + Thread.sleep((long) (1000.0 / config.ODOMETRY_FREQUENCY)); if (phoenixSignals.length > 0) BaseStatusSignal.refreshAll(phoenixSignals); } } catch (InterruptedException e) { @@ -128,7 +134,7 @@ public void run() { } // Save new data to queues - AkitSwerveDrive.odometryLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); try { // Sample timestamp is current FPGA time minus average CAN latency // Default timestamps from Phoenix are NOT compatible with @@ -153,7 +159,7 @@ public void run() { timestampQueues.get(i).offer(timestamp); } } finally { - AkitSwerveDrive.odometryLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); } } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/SparkOdometryThread.java b/src/main/java/org/frc5010/common/drive/swerve/akit/SparkOdometryThread.java index 9f664a42..e3de0aa4 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/SparkOdometryThread.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/SparkOdometryThread.java @@ -9,13 +9,13 @@ import com.revrobotics.REVLibError; import com.revrobotics.spark.SparkBase; -import edu.wpi.first.wpilibj.Notifier; import edu.wpi.first.wpilibj.RobotController; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.function.DoubleSupplier; +import org.frc5010.common.drive.swerve.SwerveDriveFunctions; /** * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. @@ -23,76 +23,44 @@ *

This version includes an overload for Spark signals, which checks for errors to ensure that * all measurements in the sample are valid. */ -public class SparkOdometryThread { +public class SparkOdometryThread extends OdometryThread { private final List sparks = new ArrayList<>(); private final List sparkSignals = new ArrayList<>(); - private final List genericSignals = new ArrayList<>(); private final List> sparkQueues = new ArrayList<>(); - private final List> genericQueues = new ArrayList<>(); - private final List> timestampQueues = new ArrayList<>(); + private static SparkOdometryThread instance; - private static SparkOdometryThread instance = null; - private Notifier notifier = new Notifier(this::run); - - public static SparkOdometryThread getInstance() { + public static void createInstance() { if (instance == null) { instance = new SparkOdometryThread(); } - return instance; } - private SparkOdometryThread() { - notifier.setName("OdometryThread"); + public static SparkOdometryThread getInstance() { + return instance; } - public void start() { - if (timestampQueues.size() > 0) { - notifier.startPeriodic(1.0 / DriveConstants.odometryFrequency); - } + private SparkOdometryThread() { + super("SparkOdometryThread"); + commonInstance = this; } /** Registers a Spark signal to be read from the thread. */ public Queue registerSignal(SparkBase spark, DoubleSupplier signal) { Queue queue = new ArrayBlockingQueue<>(20); - AkitSwerveDrive.odometryLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); try { sparks.add(spark); sparkSignals.add(signal); sparkQueues.add(queue); } finally { - AkitSwerveDrive.odometryLock.unlock(); - } - return queue; - } - - /** Registers a generic signal to be read from the thread. */ - public Queue registerSignal(DoubleSupplier signal) { - Queue queue = new ArrayBlockingQueue<>(20); - AkitSwerveDrive.odometryLock.lock(); - try { - genericSignals.add(signal); - genericQueues.add(queue); - } finally { - AkitSwerveDrive.odometryLock.unlock(); - } - return queue; - } - - /** Returns a new queue that returns timestamp values for each sample. */ - public Queue makeTimestampQueue() { - Queue queue = new ArrayBlockingQueue<>(20); - AkitSwerveDrive.odometryLock.lock(); - try { - timestampQueues.add(queue); - } finally { - AkitSwerveDrive.odometryLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); } return queue; } - private void run() { - // Save new data to queues - AkitSwerveDrive.odometryLock.lock(); + @Override + protected void runThreadLogic() { + SwerveDriveFunctions.odometryLock.lock(); try { // Get sample timestamp double timestamp = RobotController.getFPGATime() / 1e6; diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/TalonFXOdometryThread.java b/src/main/java/org/frc5010/common/drive/swerve/akit/TalonFXOdometryThread.java new file mode 100644 index 00000000..9fae4ab9 --- /dev/null +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/TalonFXOdometryThread.java @@ -0,0 +1,145 @@ +// Copyright (c) 2021-2025 Littleton Robotics +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by a BSD +// license that can be found in the LICENSE file +// at the root directory of this project. + +package org.frc5010.common.drive.swerve.akit; + +import com.ctre.phoenix6.BaseStatusSignal; +import com.ctre.phoenix6.StatusSignal; +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj.RobotController; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; +import org.frc5010.common.drive.swerve.SwerveDriveFunctions; + +/** + * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. + * + *

This version is intended for Phoenix 6 devices on both the RIO and CANivore buses. When using + * a CANivore, the thread uses the "waitForAll" blocking method to enable more consistent sampling. + * This also allows Phoenix Pro users to benefit from lower latency between devices using CANivore + * time synchronization. + */ +public class TalonFXOdometryThread extends OdometryThread { + private final Lock signalsLock = + new ReentrantLock(); // Prevents conflicts when registering signals + private BaseStatusSignal[] phoenixSignals = new BaseStatusSignal[0]; + private final List> phoenixQueues = new ArrayList<>(); + AkitSwerveConfig config; + + private static boolean isCANFD; + private static TalonFXOdometryThread instance = null; + + public static TalonFXOdometryThread getInstance() { + return instance; + } + + public static void createInstance(AkitSwerveConfig config) { + if (instance == null) { + instance = new TalonFXOdometryThread(config); + } + } + + private TalonFXOdometryThread(AkitSwerveConfig config) { + super("TalonFXOdometryThread"); + this.config = config; + isCANFD = config.getCANBus().isNetworkFD(); + commonInstance = this; + } + + @Override + public void start() { + if (!timestampQueues.isEmpty() && RobotBase.isReal()) { + super.start(); + } + } + + /** Registers a Phoenix signal to be read from the thread. */ + public Queue registerSignal(StatusSignal signal) { + Queue queue = new ArrayBlockingQueue<>(20); + signalsLock.lock(); + SwerveDriveFunctions.odometryLock.lock(); + try { + BaseStatusSignal[] newSignals = new BaseStatusSignal[phoenixSignals.length + 1]; + System.arraycopy(phoenixSignals, 0, newSignals, 0, phoenixSignals.length); + newSignals[phoenixSignals.length] = signal; + phoenixSignals = newSignals; + phoenixQueues.add(queue); + } finally { + signalsLock.unlock(); + SwerveDriveFunctions.odometryLock.unlock(); + } + return queue; + } + + /** Returns a new queue that returns timestamp values for each sample. */ + public Queue makeTimestampQueue() { + Queue queue = new ArrayBlockingQueue<>(20); + SwerveDriveFunctions.odometryLock.lock(); + try { + timestampQueues.add(queue); + } finally { + SwerveDriveFunctions.odometryLock.unlock(); + } + return queue; + } + + @Override + public void runThreadLogic() { + // Wait for updates from all signals + signalsLock.lock(); + try { + if (isCANFD && phoenixSignals.length > 0) { + BaseStatusSignal.waitForAll(2.0 / config.ODOMETRY_FREQUENCY, phoenixSignals); + } else { + // "waitForAll" does not support blocking on multiple signals with a bus + // that is not CAN FD, regardless of Pro licensing. No reasoning for this + // behavior is provided by the documentation. + Thread.sleep((long) (1000.0 / config.ODOMETRY_FREQUENCY)); + if (phoenixSignals.length > 0) BaseStatusSignal.refreshAll(phoenixSignals); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + signalsLock.unlock(); + } + + // Save new data to queues + SwerveDriveFunctions.odometryLock.lock(); + try { + // Sample timestamp is current FPGA time minus average CAN latency + // Default timestamps from Phoenix are NOT compatible with + // FPGA timestamps, this solution is imperfect but close + double timestamp = RobotController.getFPGATime() / 1e6; + double totalLatency = 0.0; + for (BaseStatusSignal signal : phoenixSignals) { + totalLatency += signal.getTimestamp().getLatency(); + } + if (phoenixSignals.length > 0) { + timestamp -= totalLatency / phoenixSignals.length; + } + + // Add new samples to queues + for (int i = 0; i < phoenixSignals.length; i++) { + phoenixQueues.get(i).offer(phoenixSignals[i].getValueAsDouble()); + } + for (int i = 0; i < genericSignals.size(); i++) { + genericQueues.get(i).offer(genericSignals.get(i).getAsDouble()); + } + for (int i = 0; i < timestampQueues.size(); i++) { + timestampQueues.get(i).offer(timestamp); + } + } finally { + SwerveDriveFunctions.odometryLock.unlock(); + } + } +} diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/util/PhoenixUtil.java b/src/main/java/org/frc5010/common/drive/swerve/akit/util/PhoenixUtil.java index 289d1918..889b351c 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/util/PhoenixUtil.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/util/PhoenixUtil.java @@ -13,8 +13,9 @@ package org.frc5010.common.drive.swerve.akit.util; -import static edu.wpi.first.units.Units.*; +import static edu.wpi.first.units.Units.KilogramSquareMeters; import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.units.Units.Volts; import com.ctre.phoenix6.StatusCode; import com.ctre.phoenix6.configs.Slot0Configs; @@ -30,9 +31,9 @@ import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.Timer; import java.util.function.Supplier; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.motorsims.SimulatedBattery; -import org.ironmaple.simulation.motorsims.SimulatedMotorController; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.motorsims.SimulatedBattery; +import swervelib.simulation.ironmaple.simulation.motorsims.SimulatedMotorController; public final class PhoenixUtil { /** Attempts to run the command until no error is produced. */ @@ -102,25 +103,14 @@ public static double[] getSimulationOdometryTimeStamps() { } /** + * Regulates a {@link SwerveModuleConstants} object for simulation. If running on a real robot, + * the input object is returned unchanged. Otherwise, simulation-specific adjustments are made to + * the module constants. The following adjustments are made: - Disable encoder offsets - Disable + * motor inversions for drive and steer motors - Disable CanCoder inversion - Adjust steer motor + * PID gains for simulation - Adjust friction voltages - Adjust steer inertia * - * - *

Regulates the {@link SwerveModuleConstants} for a single module.

- * - *

This method applies specific adjustments to the {@link SwerveModuleConstants} for simulation - * purposes. These changes have no effect on real robot operations and address known simulation - * bugs: - * - *

    - *
  • Inverted Drive Motors: Prevents drive PID issues caused by inverted - * configurations. - *
  • Non-zero CanCoder Offsets: Fixes potential module state optimization - * issues. - *
  • Steer Motor PID: Adjusts PID values tuned for real robots to improve - * simulation performance. - *
- * - *

Note:This function is skipped when running on a real robot, ensuring no impact on constants - * used on real robot hardware.

+ * @param moduleConstants module constants to regulate + * @return regulated module constants */ public static SwerveModuleConstants regulateModuleConstantForSimulation( SwerveModuleConstants moduleConstants) { diff --git a/src/main/java/org/frc5010/common/drive/swerve_utils/PathConstraints5010.java b/src/main/java/org/frc5010/common/drive/swerve_utils/PathConstraints5010.java index e2e21a6d..41e89551 100644 --- a/src/main/java/org/frc5010/common/drive/swerve_utils/PathConstraints5010.java +++ b/src/main/java/org/frc5010/common/drive/swerve_utils/PathConstraints5010.java @@ -17,7 +17,7 @@ * Kinematic path following constraints * * @param maxVelocityMPS Max linear velocity (M/S) - * @param maxAccelerationMPSSq Max linear acceleration (M/S^2) + * @param maxPhysicalAcceleration Max physical acceleration (M/S^2) * @param maxAngularVelocityRadPerSec Max angular velocity (Rad/S) * @param maxAngularAccelerationRadPerSecSq Max angular acceleration (Rad/S^2) * @param nominalVoltageVolts The nominal battery voltage (Volts) @@ -38,15 +38,22 @@ public record PathConstraints5010( double maxAngularAccelerationRadPerSecSq, double nominalVoltageVolts, boolean unlimited) { + /** - * Kinematic path following constraints - * - * @param maxVelocity Max linear velocity - * @param maxAcceleration Max linear acceleration - * @param maxAngularVelocity Max angular velocity - * @param maxAngularAcceleration Max angular acceleration - * @param nominalVoltage The nominal battery voltage - * @param unlimited Should the constraints be unlimited + * @param maxVelocity + * @param maxPhysicalAcceleration + * @param maxForwardAcceleration + * @param maxBackwardAcceleration + * @param maxLeftAcceleration + * @param maxRightAcceleration + * @param maxForwardVelocity + * @param maxBackwardVelocity + * @param maxLeftVelocity + * @param maxRightVelocity + * @param maxAngularVelocity + * @param maxAngularAcceleration + * @param nominalVoltage + * @param unlimited */ public PathConstraints5010( LinearVelocity maxVelocity, diff --git a/src/main/java/org/frc5010/common/motors/MotorConstants.java b/src/main/java/org/frc5010/common/motors/MotorConstants.java index 621fe85f..3bbe3e53 100644 --- a/src/main/java/org/frc5010/common/motors/MotorConstants.java +++ b/src/main/java/org/frc5010/common/motors/MotorConstants.java @@ -21,10 +21,25 @@ public static enum Motor { RPM.of(11000), (Integer numberOfMotors) -> DCMotor.getNeo550(numberOfMotors), 42), + NeoVortex( + Amps.of(40), + RPM.of(6000), + (Integer numberOfMotors) -> DCMotor.getNeoVortex(numberOfMotors), + 42), KrakenX60( Amps.of(40), RPM.of(6000), (Integer numberOfMotors) -> DCMotor.getKrakenX60(numberOfMotors), + 1023), + KrakenX60Foc( + Amps.of(40), + RPM.of(6000), + (Integer numberOfMotors) -> DCMotor.getKrakenX60Foc(numberOfMotors), + 1023), + KrakenX44( + Amps.of(40), + RPM.of(6000), + (Integer numberOfMotors) -> DCMotor.getKrakenX44(numberOfMotors), 1023); public Current currentLimit = Amps.of(20); diff --git a/src/main/java/org/frc5010/common/motors/MotorFactory.java b/src/main/java/org/frc5010/common/motors/MotorFactory.java index a0678c36..e32c9c93 100644 --- a/src/main/java/org/frc5010/common/motors/MotorFactory.java +++ b/src/main/java/org/frc5010/common/motors/MotorFactory.java @@ -55,17 +55,33 @@ public static Color getNextVisualColor() { public static GenericMotorController Spark(int canId, Motor config) { switch (config) { + case KrakenX44: case KrakenX60: - throw new IllegalArgumentException("Sparks can not use KrakenX60 config"); + case NeoVortex: + throw new IllegalArgumentException("Sparks can not use " + config + " config"); default: } - return new GenericRevBrushlessMotor(canId, config); + return new GenericRevBrushlessMotor(canId, config, true); + } + + public static GenericMotorController SparkFlex(int canId, Motor config) { + switch (config) { + case KrakenX44: + case KrakenX60: + case Neo550: + case Neo: + throw new IllegalArgumentException("Sparks can not use " + config + " config"); + default: + } + return new GenericRevBrushlessMotor(canId, config, false); } public static GenericMotorController Thrifty(int canId, Motor config) { switch (config) { + case KrakenX44: case KrakenX60: - throw new IllegalArgumentException("Thrifty Novas can not use KrakenX60 config"); + case NeoVortex: + throw new IllegalArgumentException("Thrifty can not use " + config + " config"); default: } return new GenericThriftyNovaMotor(canId, config); @@ -75,6 +91,10 @@ public static GenericMotorController TalonFX(int canId, Motor config) { switch (config) { case KrakenX60: return new GenericTalonFXMotor(canId, config); + case KrakenX44: + return new GenericTalonFXMotor(canId, config); + case KrakenX60Foc: + return new GenericTalonFXMotor(canId, config); default: throw new IllegalArgumentException("TalonFX can not use " + config + " config"); } diff --git a/src/main/java/org/frc5010/common/motors/SystemIdentification.java b/src/main/java/org/frc5010/common/motors/SystemIdentification.java index fb68bdad..9f9723cf 100644 --- a/src/main/java/org/frc5010/common/motors/SystemIdentification.java +++ b/src/main/java/org/frc5010/common/motors/SystemIdentification.java @@ -16,11 +16,21 @@ import edu.wpi.first.units.measure.MutVoltage; import edu.wpi.first.units.measure.Voltage; import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.Timer; +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.SubsystemBase; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Config; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.LinkedList; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.frc5010.common.arch.GenericSubsystem; import org.frc5010.common.sensors.encoder.GenericEncoder; import yams.motorcontrollers.SmartMotorController; @@ -65,7 +75,28 @@ public static SysIdRoutine rpmSysIdRoutine( SmartMotorController motor, String motorName, SubsystemBase subsystemBase) { return new SysIdRoutine( - new Config(Volts.of(1).div(Seconds.of(1)), Volts.of(1), Seconds.of(10)), + new Config(Volts.of(1).div(Seconds.of(1)), Volts.of(4), Seconds.of(10)), + new SysIdRoutine.Mechanism( + (Voltage voltage) -> + motor.setDutyCycle(voltage.in(Volts) / RobotController.getBatteryVoltage()), + log -> { + motor.updateTelemetry(); + motor.simIterate(); + log.motor(motorName) + .voltage( + m_appliedVoltage.mut_replace( + motor.getDutyCycle() * RobotController.getBatteryVoltage(), Volts)) + .angularPosition(m_distance.mut_replace(motor.getMechanismPosition())) + .angularVelocity(m_velocity.mut_replace(motor.getMechanismVelocity())); + }, + subsystemBase)); + } + + public static SysIdRoutine angleSysIdRoutine( + SmartMotorController motor, String motorName, SubsystemBase subsystemBase) { + + return new SysIdRoutine( + new Config(Volts.of(1).div(Seconds.of(1)), Volts.of(4), Seconds.of(10)), new SysIdRoutine.Mechanism( (Voltage voltage) -> motor.setDutyCycle(voltage.in(Volts) / RobotController.getBatteryVoltage()), @@ -129,6 +160,196 @@ public static Command getSysIdDynamicBackward(SysIdRoutine routine) { return getSysIdDynamic(routine, SysIdRoutine.Direction.kReverse); } + private static final double FF_START_DELAY = 2.0; // Secs + private static final double FF_RAMP_RATE = 0.1; // Volts/Sec + /** + * Measures the velocity feedforward constants for the motors. + * + *

This command should only be used in voltage control mode. + * + * @param subsystem the swerve drivetrain subsystem to characterize + * @param characterizer consumer that accepts voltage values to apply to motors + * @param velocitySupplier supplier that returns the current velocity for measurement + * @return a command that performs feedforward characterization and logs results + */ + public static Command feedforwardCharacterization( + GenericSubsystem subsystem, + Consumer characterizer, + Supplier velocitySupplier) { + List velocitySamples = new LinkedList<>(); + List voltageSamples = new LinkedList<>(); + List timeSamples = new LinkedList<>(); + Timer timer = new Timer(); + + return Commands.sequence( + // Reset data + Commands.runOnce( + () -> { + velocitySamples.clear(); + voltageSamples.clear(); + timeSamples.clear(); + }), + + // Allow modules to orient + Commands.run( + () -> { + characterizer.accept(Volts.of(0.0)); + }, + subsystem) + .withTimeout(FF_START_DELAY), + + // Start timer + Commands.runOnce(timer::restart), + + // Accelerate and gather data + Commands.run( + () -> { + double voltage = timer.get() * FF_RAMP_RATE; + characterizer.accept(Volts.of(voltage)); + velocitySamples.add(velocitySupplier.get()); + voltageSamples.add(voltage); + timeSamples.add(timer.get()); + }, + subsystem) + + // When cancelled, calculate and print results + .finallyDo( + () -> { + double[] coefficients = + computeFeedforwardCoefficients(velocitySamples, voltageSamples, timeSamples); + double kS = coefficients[0]; + double kV = coefficients[1]; + double kA = coefficients[2]; + + NumberFormat formatter = new DecimalFormat("#0.00000"); + System.out.println("********** FF Characterization Results **********"); + System.out.println("\tkS: " + formatter.format(kS)); + System.out.println("\tkV: " + formatter.format(kV)); + System.out.println("\tkA: " + formatter.format(kA)); + SmartDashboard.putNumber("Characterization/Feedforward/kS", kS); + SmartDashboard.putNumber("Characterization/Feedforward/kV", kV); + SmartDashboard.putNumber("Characterization/Feedforward/kA", kA); + })); + } + + static double[] computeFeedforwardCoefficients( + List velocitySamples, List voltageSamples, List timeSamples) { + int n = Math.min(velocitySamples.size(), voltageSamples.size()); + if (n == 0) { + return new double[] {0.0, 0.0, 0.0}; + } + + double sumX = 0.0; + double sumY = 0.0; + double sumXY = 0.0; + double sumX2 = 0.0; + for (int i = 0; i < n; i++) { + double velocity = velocitySamples.get(i); + double voltage = voltageSamples.get(i); + sumX += velocity; + sumY += voltage; + sumXY += velocity * voltage; + sumX2 += velocity * velocity; + } + double denom = (n * sumX2 - sumX * sumX); + double kS = denom != 0.0 ? (sumY * sumX2 - sumX * sumXY) / denom : 0.0; + double kV = denom != 0.0 ? (n * sumXY - sumX * sumY) / denom : 0.0; + double kA = 0.0; + + if (timeSamples == null || timeSamples.size() < n) { + return new double[] {kS, kV, kA}; + } + + int m = 0; + double s00 = 0.0; + double s01 = 0.0; + double s02 = 0.0; + double s11 = 0.0; + double s12 = 0.0; + double s22 = 0.0; + double t0 = 0.0; + double t1 = 0.0; + double t2 = 0.0; + for (int i = 1; i < n; i++) { + double dt = timeSamples.get(i) - timeSamples.get(i - 1); + if (dt <= 1e-6) { + continue; + } + double velocity = velocitySamples.get(i); + double accel = (velocity - velocitySamples.get(i - 1)) / dt; + double voltage = voltageSamples.get(i); + s00 += 1.0; + s01 += velocity; + s02 += accel; + s11 += velocity * velocity; + s12 += velocity * accel; + s22 += accel * accel; + t0 += voltage; + t1 += velocity * voltage; + t2 += accel * voltage; + m++; + } + + if (m >= 3) { + double[][] a = { + {s00, s01, s02}, + {s01, s11, s12}, + {s02, s12, s22} + }; + double[] b = {t0, t1, t2}; + double[] solution = solve3x3(a, b); + if (solution != null) { + kS = solution[0]; + kV = solution[1]; + kA = solution[2]; + } + } + + return new double[] {kS, kV, kA}; + } + + private static double[] solve3x3(double[][] a, double[] b) { + double[][] m = { + {a[0][0], a[0][1], a[0][2], b[0]}, + {a[1][0], a[1][1], a[1][2], b[1]}, + {a[2][0], a[2][1], a[2][2], b[2]} + }; + + for (int i = 0; i < 3; i++) { + int pivot = i; + for (int r = i + 1; r < 3; r++) { + if (Math.abs(m[r][i]) > Math.abs(m[pivot][i])) { + pivot = r; + } + } + if (Math.abs(m[pivot][i]) < 1e-9) { + return null; + } + if (pivot != i) { + double[] temp = m[i]; + m[i] = m[pivot]; + m[pivot] = temp; + } + + double divisor = m[i][i]; + for (int c = i; c < 4; c++) { + m[i][c] /= divisor; + } + + for (int r = 0; r < 3; r++) { + if (r == i) { + continue; + } + double factor = m[r][i]; + for (int c = i; c < 4; c++) { + m[r][c] -= factor * m[i][c]; + } + } + } + + return new double[] {m[0][3], m[1][3], m[2][3]}; + } + public static Command getSysIdFullCommand( SysIdRoutine routine, double quasistaticTimeout, double dynamicTimeout, double delay) { return getSysIdQuasistaticForward(routine) @@ -140,4 +361,36 @@ public static Command getSysIdFullCommand( .andThen(Commands.waitSeconds(delay)) .andThen(getSysIdDynamicBackward(routine).withTimeout(dynamicTimeout)); } + + public static Command getSysIdFullCommand( + SysIdRoutine routine, + double quasistaticTimeout, + double dynamicTimeout, + double delay, + BooleanSupplier isAtMax, + BooleanSupplier isAtMin, + Runnable stopMotor) { + return getSysIdQuasistaticForward(routine) + .until(isAtMax) + .finallyDo(stopMotor) + .withTimeout(quasistaticTimeout) + .andThen(Commands.waitSeconds(delay)) + .andThen( + getSysIdQuasistaticBackward(routine) + .until(isAtMin) + .finallyDo(stopMotor) + .withTimeout(quasistaticTimeout)) + .andThen(Commands.waitSeconds(delay)) + .andThen( + getSysIdDynamicForward(routine) + .until(isAtMax) + .finallyDo(stopMotor) + .withTimeout(dynamicTimeout)) + .andThen(Commands.waitSeconds(delay)) + .andThen( + getSysIdDynamicBackward(routine) + .until(isAtMin) + .finallyDo(stopMotor) + .withTimeout(dynamicTimeout)); + } } diff --git a/src/main/java/org/frc5010/common/motors/control/RevSparkController.java b/src/main/java/org/frc5010/common/motors/control/RevSparkController.java index 3c0ac410..834ed958 100644 --- a/src/main/java/org/frc5010/common/motors/control/RevSparkController.java +++ b/src/main/java/org/frc5010/common/motors/control/RevSparkController.java @@ -5,10 +5,10 @@ package org.frc5010.common.motors.control; import com.revrobotics.spark.ClosedLoopSlot; +import com.revrobotics.spark.FeedbackSensor; import com.revrobotics.spark.SparkBase.ControlType; import com.revrobotics.spark.SparkClosedLoopController; import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; import com.revrobotics.spark.config.SparkMaxConfig; import org.frc5010.common.constants.GenericPID; import org.frc5010.common.constants.MotorFeedFwdConstants; @@ -67,7 +67,7 @@ public void setD(double d) { @Override public void setF(double f) { pidfConfig.setkF(f); - cfg.closedLoop.velocityFF(f); + cfg.closedLoop.feedForward.kV(f); motor.updateConfig(cfg); } @@ -95,14 +95,14 @@ public void setOutputRange(double min, double max) { @Override public void setReference(double reference) { this.reference = reference; - controller.setReference(reference, sparkControlType); + controller.setSetpoint(reference, sparkControlType); } @Override public void setReference(double reference, PIDControlType controlType, double feedforward) { setControlType(controlType); this.reference = reference; - controller.setReference(feedforward, sparkControlType, ClosedLoopSlot.kSlot0, feedforward); + controller.setSetpoint(feedforward, sparkControlType, ClosedLoopSlot.kSlot0, feedforward); } @Override @@ -239,7 +239,7 @@ public void configureAbsoluteControl(double offset, boolean inverted, double min @Override public void setProfiledMaxVelocity(double maxVelocity) { - cfg.closedLoop.maxMotion.maxVelocity(maxVelocity); + cfg.closedLoop.maxMotion.cruiseVelocity(maxVelocity); } @Override diff --git a/src/main/java/org/frc5010/common/motors/control/ThriftyNovaController.java b/src/main/java/org/frc5010/common/motors/control/ThriftyNovaController.java index d767a5f5..348ec1ac 100644 --- a/src/main/java/org/frc5010/common/motors/control/ThriftyNovaController.java +++ b/src/main/java/org/frc5010/common/motors/control/ThriftyNovaController.java @@ -4,8 +4,8 @@ package org.frc5010.common.motors.control; -import com.thethriftybot.ThriftyNova; -import com.thethriftybot.ThriftyNova.EncoderType; +import com.thethriftybot.devices.ThriftyNova; +import com.thethriftybot.devices.ThriftyNova.EncoderType; import edu.wpi.first.math.controller.PIDController; import org.frc5010.common.constants.GenericPID; import org.frc5010.common.constants.MotorFeedFwdConstants; diff --git a/src/main/java/org/frc5010/common/motors/function/PercentControlMotor.java b/src/main/java/org/frc5010/common/motors/function/PercentControlMotor.java index f79cc7d4..7c1a3bfb 100644 --- a/src/main/java/org/frc5010/common/motors/function/PercentControlMotor.java +++ b/src/main/java/org/frc5010/common/motors/function/PercentControlMotor.java @@ -98,4 +98,8 @@ public void simulationUpdate() { RoboRioSim.setVInVoltage( BatterySim.calculateDefaultBatteryLoadedVoltage(simMotor.getCurrentDrawAmps())); } + + public GenericMotorController getMotorcontroller() { + throw new UnsupportedOperationException("Unimplemented method 'getMotorcontroller'"); + } } diff --git a/src/main/java/org/frc5010/common/motors/hardware/GenericRevBrushlessMotor.java b/src/main/java/org/frc5010/common/motors/hardware/GenericRevBrushlessMotor.java index 4ab411ba..4f0d1d56 100644 --- a/src/main/java/org/frc5010/common/motors/hardware/GenericRevBrushlessMotor.java +++ b/src/main/java/org/frc5010/common/motors/hardware/GenericRevBrushlessMotor.java @@ -7,15 +7,17 @@ import static edu.wpi.first.units.Units.Amps; import static edu.wpi.first.units.Units.Seconds; +import com.revrobotics.PersistMode; import com.revrobotics.REVLibError; +import com.revrobotics.ResetMode; +import com.revrobotics.sim.SparkFlexSim; import com.revrobotics.sim.SparkMaxSim; +import com.revrobotics.spark.FeedbackSensor; import com.revrobotics.spark.SparkBase; -import com.revrobotics.spark.SparkBase.PersistMode; -import com.revrobotics.spark.SparkBase.ResetMode; import com.revrobotics.spark.SparkClosedLoopController; +import com.revrobotics.spark.SparkFlex; import com.revrobotics.spark.SparkLowLevel.MotorType; import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; import com.revrobotics.spark.config.SparkMaxConfig; import edu.wpi.first.math.system.plant.DCMotor; @@ -44,13 +46,15 @@ /** A class for a generic REV brushless motor */ public class GenericRevBrushlessMotor implements GenericMotorController { /** {@link SparkMax} Instance. */ - private final SparkMax motor; + private final SparkBase motor; /** The current limit */ protected int currentLimit; /** The simulated instance of the motor */ protected DCMotor motorSim; /** The maximum angular velocity */ protected AngularVelocity maxRPM; + /** MaxOrFlex */ + protected boolean maxOrFlex; /** The configuration of the motor */ protected Motor config; @@ -68,6 +72,15 @@ public class GenericRevBrushlessMotor implements GenericMotorController { private RevSparkController controller; + public GenericRevBrushlessMotor(int port, boolean maxOrFlex) { + this.maxOrFlex = maxOrFlex; + if (maxOrFlex) { + motor = new SparkMax(port, MotorType.kBrushless); + } else { + motor = new SparkFlex(port, MotorType.kBrushless); + } + } + /** * Constructor for a generic REV brushless motor * @@ -75,13 +88,18 @@ public class GenericRevBrushlessMotor implements GenericMotorController { * @param config the configuration * @param currentLimit the current limit */ - public GenericRevBrushlessMotor(int port, Motor config, Current currentLimit) { - this(port, config); + public GenericRevBrushlessMotor(int port, Motor config, Current currentLimit, boolean maxOrFlex) { + this(port, config, maxOrFlex); setCurrentLimit(currentLimit); } - public GenericRevBrushlessMotor(int port, Motor config) { - motor = new SparkMax(port, MotorType.kBrushless); + public GenericRevBrushlessMotor(int port, Motor config, boolean maxOrFlex) { + this.maxOrFlex = maxOrFlex; + if (maxOrFlex) { + motor = new SparkMax(port, MotorType.kBrushless); + } else { + motor = new SparkFlex(port, MotorType.kBrushless); + } this.config = config; factoryDefaults(); clearStickyFaults(); @@ -105,7 +123,7 @@ public GenericRevBrushlessMotor(int port, Motor config) { @Override public GenericMotorController duplicate(int port) { GenericMotorController duplicate = - new GenericRevBrushlessMotor(port, config, Amps.of(currentLimit)); + new GenericRevBrushlessMotor(port, config, Amps.of(currentLimit), maxOrFlex); return duplicate; } @@ -457,7 +475,11 @@ public double getOutputCurrent() { @Override public void setMotorSimulationType(DCMotor motorSimulationType) { motorSim = motorSimulationType; - encoder.setSimulation(new SparkMaxSim(motor, motorSim)); + if (maxOrFlex) { + encoder.setSimulation(new SparkMaxSim(((SparkMax) motor), motorSim)); + } else { + encoder.setSimulation(new SparkFlexSim(((SparkFlex) motor), motorSim)); + } } /** diff --git a/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXMotor.java b/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXMotor.java index b44cff0d..71f45396 100644 --- a/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXMotor.java +++ b/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXMotor.java @@ -7,6 +7,7 @@ import static edu.wpi.first.units.Units.Amps; import static edu.wpi.first.units.Units.Volts; +import com.ctre.phoenix6.CANBus; import com.ctre.phoenix6.SignalLogger; import com.ctre.phoenix6.configs.TalonFXConfiguration; import com.ctre.phoenix6.configs.TalonFXConfigurator; @@ -15,6 +16,7 @@ import com.ctre.phoenix6.controls.VoltageOut; import com.ctre.phoenix6.hardware.TalonFX; import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.MotorAlignmentValue; import com.ctre.phoenix6.signals.NeutralModeValue; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.units.measure.AngularVelocity; @@ -68,15 +70,18 @@ public class GenericTalonFXMotor implements GenericMotorController { /** Configuration */ protected Motor config; + public GenericTalonFXMotor(int id, String canbus) { + motor = new TalonFX(id, new CANBus(canbus)); + } /** * Construct the TalonFX swerve motor given the ID and CANBus. * * @param id ID of the TalonFX on the CANBus. + * @param config Motor configuration. * @param canbus CANBus on which the TalonFX is on. - * @param isDriveMotor Whether the motor is a drive or steering motor. */ public GenericTalonFXMotor(int id, Motor config, String canbus) { - motor = new TalonFX(id, canbus); + motor = new TalonFX(id, new CANBus(canbus)); this.cfg = motor.getConfigurator(); factoryDefaults(); @@ -175,7 +180,8 @@ public GenericMotorController setSlewRate(double rate) { */ @Override public GenericMotorController setFollow(GenericMotorController motor) { - this.motor.setControl(new Follower(((TalonFX) motor.getMotor()).getDeviceID(), false)); + this.motor.setControl( + new Follower(((TalonFX) motor.getMotor()).getDeviceID(), MotorAlignmentValue.Aligned)); return this; } @@ -188,7 +194,10 @@ public GenericMotorController setFollow(GenericMotorController motor) { */ @Override public GenericMotorController setFollow(GenericMotorController motor, boolean inverted) { - this.motor.setControl(new Follower(((TalonFX) motor.getMotor()).getDeviceID(), inverted)); + this.motor.setControl( + new Follower( + ((TalonFX) motor.getMotor()).getDeviceID(), + inverted ? MotorAlignmentValue.Opposed : MotorAlignmentValue.Aligned)); return this; } diff --git a/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXSMotor.java b/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXSMotor.java new file mode 100644 index 00000000..830df53a --- /dev/null +++ b/src/main/java/org/frc5010/common/motors/hardware/GenericTalonFXSMotor.java @@ -0,0 +1,530 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.motors.hardware; + +import static edu.wpi.first.units.Units.Amps; +import static edu.wpi.first.units.Units.Volts; + +import com.ctre.phoenix6.CANBus; +import com.ctre.phoenix6.SignalLogger; +import com.ctre.phoenix6.configs.TalonFXSConfiguration; +import com.ctre.phoenix6.configs.TalonFXSConfigurator; +import com.ctre.phoenix6.controls.ControlRequest; +import com.ctre.phoenix6.controls.Follower; +import com.ctre.phoenix6.controls.VoltageOut; +import com.ctre.phoenix6.hardware.TalonFX; +import com.ctre.phoenix6.hardware.TalonFXS; +import com.ctre.phoenix6.signals.InvertedValue; +import com.ctre.phoenix6.signals.MotorAlignmentValue; +import com.ctre.phoenix6.signals.NeutralModeValue; +import edu.wpi.first.math.system.plant.DCMotor; +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Voltage; +import edu.wpi.first.wpilibj.RobotBase; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; +import java.util.Optional; +import org.frc5010.common.motors.GenericMotorController; +import org.frc5010.common.motors.GenericPIDController; +import org.frc5010.common.motors.MotorConstants.Motor; +import org.frc5010.common.motors.control.TalonFXController; +import org.frc5010.common.sensors.encoder.GenericEncoder; +import org.frc5010.common.sensors.encoder.TalonFXEncoder; +import yams.motorcontrollers.SmartMotorController; +import yams.motorcontrollers.SmartMotorControllerConfig; +import yams.motorcontrollers.remote.TalonFXSWrapper; + +/** A class for a generic TalonFX motor */ +public class GenericTalonFXSMotor implements GenericMotorController { + /** Wait time for status frames to show up. */ + public static double STATUS_TIMEOUT_SECONDS = 0.02; + /** Factory default already occurred. */ + private final boolean factoryDefaultOccurred = false; + /** TalonFX motor controller. */ + private final TalonFXS motor; + /** TalonFX controller */ + protected TalonFXController controller; + /** TalonFX encoder */ + protected TalonFXEncoder encoder; + /** Current TalonFX configuration. */ + private TalonFXSConfiguration configuration = new TalonFXSConfiguration(); + /** Current TalonFX Configurator. */ + private TalonFXSConfigurator cfg; + /** Current motor current limit */ + protected int motorCurrentLimit; + /** Current motor supply current limit */ + protected int supplyCurrentLimit; + /** Current controller current limit */ + protected int controllerCurrentLimit; + /** Enable FOC */ + protected boolean enableFOC = true; + + /** DCMotor simulation */ + protected DCMotor motorSim; + + /** Max RPM */ + protected AngularVelocity maxRPM; + + /** Configuration */ + protected Motor config; + + public GenericTalonFXSMotor(int id, String canbus) { + motor = new TalonFXS(id, new CANBus(canbus)); + } + /** + * Construct the TalonFX swerve motor given the ID and CANBus. + * + * @param id ID of the TalonFX on the CANBus. + * @param config Motor configuration. + * @param canbus CANBus on which the TalonFX is on. + */ + public GenericTalonFXSMotor(int id, Motor config, String canbus) { + motor = new TalonFXS(id, new CANBus(canbus)); + this.cfg = motor.getConfigurator(); + + factoryDefaults(); + clearStickyFaults(); + setCurrentLimit(config.currentLimit); + setSupplyCurrent(Amps.of(40)); + setMotorSimulationType(config.getMotorSimulationType()); + setMaxRPM(config.maxRpm); + this.config = config; + // encoder = new TalonFXEncoder(this); + // controller = new TalonFXController(this); + } + + public GenericTalonFXSMotor(int canId, Motor config) { + this(canId, config, ""); + } + + /** + * Creates a duplicate of the current motor controller with the specified port. + * + * @param port The port number for the new motor controller. + * @return A new instance of MotorController5010 with the same configuration. + */ + @Override + public GenericMotorController duplicate(int port) { + GenericMotorController duplicate = new GenericTalonFXMotor(port, config); + return duplicate; + } + + /** Configure the factory defaults. */ + @Override + public void factoryDefaults() { + if (!factoryDefaultOccurred) { + configuration.MotorOutput.NeutralMode = NeutralModeValue.Brake; + configuration.ClosedLoopGeneral.ContinuousWrap = true; + cfg.apply(configuration); + } + } + + /** Clear the sticky faults on the motor controller. */ + @Override + public void clearStickyFaults() { + motor.clearStickyFaults(); + } + + /** + * Set the current limit for the swerve drive motor, remember this may cause jumping if used in + * conjunction with voltage compensation. This is useful to protect the motor from current spikes. + * + * @param limit Current limit in AMPS at free speed. + * @return a reference to the current MotorController5010 instance + */ + @Override + public GenericMotorController setCurrentLimit(Current limit) { + motorCurrentLimit = (int) limit.in(Amps); + refreshCurrentLimits(); + + return this; + } + + private void refreshCurrentLimits() { + cfg.refresh(configuration.CurrentLimits); + cfg.apply( + configuration.CurrentLimits.withSupplyCurrentLimit(supplyCurrentLimit) + .withStatorCurrentLimit(motorCurrentLimit) + .withSupplyCurrentLimitEnable(0 != supplyCurrentLimit) + .withStatorCurrentLimitEnable(0 != motorCurrentLimit)); + } + + public GenericMotorController setSupplyCurrent(Current limit) { + supplyCurrentLimit = (int) limit.in(Amps); + refreshCurrentLimits(); + + return this; + } + + /** + * Set the maximum rate the open/closed loop output can change by. + * + * @param rate Time in seconds to go from 0 to full throttle. + * @return a reference to the current MotorController5010 instance + */ + @Override + public GenericMotorController setSlewRate(double rate) { + cfg.refresh(configuration.ClosedLoopRamps); + cfg.apply(configuration.ClosedLoopRamps.withVoltageClosedLoopRampPeriod(rate)); + return this; + } + + /** + * Set the motor to follow another motor controller. This is useful for master/slave + * configurations or for having a motor follow another motor without having to duplicate code. + * + * @param motor The motor controller to follow. + * @return a reference to the current MotorController5010 instance + */ + @Override + public GenericMotorController setFollow(GenericMotorController motor) { + this.motor.setControl( + new Follower(((TalonFX) motor.getMotor()).getDeviceID(), MotorAlignmentValue.Aligned)); + return this; + } + + /** + * Set the motor to follow another motor controller with the option to invert the follower. + * + * @param motor The motor controller to follow. + * @param inverted Whether the follower should be inverted or not. + * @return a reference to the current MotorController5010 instance + */ + @Override + public GenericMotorController setFollow(GenericMotorController motor, boolean inverted) { + this.motor.setControl( + new Follower( + ((TalonFX) motor.getMotor()).getDeviceID(), + inverted ? MotorAlignmentValue.Opposed : MotorAlignmentValue.Aligned)); + return this; + } + + /** + * Set the motor to be inverted. + * + * @param inverted State of inversion. + */ + @Override + public void setInverted(boolean inverted) { + cfg.refresh(configuration.MotorOutput); + configuration.MotorOutput.withInverted( + !inverted ? InvertedValue.CounterClockwise_Positive : InvertedValue.Clockwise_Positive); + cfg.apply(configuration.MotorOutput); + } + + /** + * Set the motor to be inverted. + * + * @param inverted Whether the motor should be inverted. + * @return a reference to the current MotorController5010 instance + */ + @Override + public GenericMotorController invert(boolean inverted) { + setInverted(inverted); + return this; + } + + /** + * Retrieves the motor encoder for the TalonFX motor. + * + * @return an instance of GenericEncoder, specifically a TalonFXEncoder associated with this + * motor. + */ + @Override + public GenericEncoder getMotorEncoder() { + return encoder; + } + + /** + * Retrieves the motor encoder for the TalonFX motor, with the specified counts per revolution. + * + * @param countsPerRev The number of counts per revolution of the motor + * @return an instance of GenericEncoder, specifically a TalonFXEncoder associated with this + * motor. + */ + @Override + public GenericEncoder createMotorEncoder(int countsPerRev) { + // GenericEncoder encoder = new TalonFXEncoder(this); + encoder.setPositionConversion(countsPerRev); + encoder.setVelocityConversion(countsPerRev); + return encoder; + } + + /** + * Retrieves the PID controller specific to the TalonFX motor. + * + * @return an instance of PIDController5010, specifically a TalonFXPID associated with this motor. + */ + @Override + public GenericPIDController getPIDController5010() { + return controller; + } + + /** + * Retrieves the motor controller itself. + * + * @return The motor controller (this object) + */ + @Override + public Object getMotor() { + return motor; + } + + /** + * Returns the default system identification routine for the motor. + * + *

This method creates a SysIdRoutine configured to prevent motor brownout by reducing the + * dynamic voltage to 4 volts. It uses the Phoenix SignalLogger class to log the state of the + * system. The mechanism implementation sets the motor control using the provided voltage output + * and is associated with the given subsystem. + * + * @param subsystemBase The subsystem that this routine is associated with + * @return A SysIdRoutine instance configured for system identification + */ + @Override + public SysIdRoutine getDefaultSysId(SubsystemBase subsystemBase) { + VoltageOut sysidControl = new VoltageOut(0); + return new SysIdRoutine( + new SysIdRoutine.Config( + null, // Default ramp rate is acceptable + Volts.of(4), // Reduce dynamic voltage to 4 to prevent motor brownout + null, // Default timeout is acceptable + // Log state with Phoenix SignalLogger class + (state) -> SignalLogger.writeString("state", state.toString())), + new SysIdRoutine.Mechanism( + (Voltage volts) -> motor.setControl(sysidControl.withOutput(volts.in(Volts))), + null, + subsystemBase)); + } + + /** + * Checks if Field-Oriented Control (FOC) is enabled for the motor. + * + * @return true if FOC is enabled, false otherwise + */ + public boolean isFOCEnabled() { + return enableFOC; + } + + /** + * Enable or disable Field-Oriented Control (FOC) for the motor. FOC allows the motor to operate + * more efficiently by adjusting the motor's magnetic field, but it also requires a position + * sensor and may not work with all motor types. FOC is disabled by default. + * + * @param enableFOC true to enable FOC, false to disable + */ + public void enableFOC(boolean enableFOC) { + this.enableFOC = enableFOC; + } + + /** + * Returns the type of motor that is being simulated. This is used for motor system identification + * and simulation. + * + * @return The type of motor being simulated. + */ + @Override + public DCMotor getMotorSimulationType() { + return motorSim; + } + + /** + * Returns the maximum revolutions per minute (RPM) the motor can achieve. + * + * @return The maximum RPM of the motor. + * @throws UnsupportedOperationException if the method is not implemented. + */ + @Override + public AngularVelocity getMaxRPM() { + return config.maxRpm; + } + + /** + * Sets the voltage compensation of the motor to the given nominal voltage. + * + *

This method does not do anything for TalonFX motors, but is included to provide a consistent + * interface with other motor types. + * + * @param nominalVoltage The nominal voltage to set for voltage compensation. + * @return This motor controller. + */ + @Override + public GenericMotorController setVoltageCompensation(double nominalVoltage) { + return this; + } + + /** + * Sets the idle mode of the motor to either brake or coast. + * + * @param isBrakeMode If true, sets the motor to brake mode; otherwise, sets it to coast mode. + * @return This motor controller instance. + */ + @Override + public GenericMotorController setMotorBrake(boolean isBrakeMode) { + motor.setNeutralMode(isBrakeMode ? NeutralModeValue.Brake : NeutralModeValue.Coast); + return this; + } + + /** + * Saves the motor controller's configuration to flash memory. This must be called after setting + * the motor controller's configuration in order to persist the changes after the robot is + * restarted. + */ + @Override + public void burnFlash() { + cfg.apply(configuration); + } + + /** + * Retrieves the voltage output of the motor controller. + * + *

This method waits for the voltage update within a specified timeout and returns the motor's + * voltage in volts. + * + * @return The voltage output of the motor in volts. + */ + @Override + public double getVoltage() { + if (RobotBase.isReal()) { + return motor.getMotorVoltage().refresh().getValue().in(Volts); + } else { + return encoder.getVoltage(); + } + } + + /** + * Gets the applied output of the motor as a double in the range of -1.0 to 1.0. + * + *

This method waits for the applied output update within a specified timeout and returns the + * motor's applied output as a double between -1 and 1. + * + * @return The motor's applied output as a double between -1 and 1. + */ + @Override + public double getAppliedOutput() { + return motor.getDutyCycle().refresh().getValue(); + } + + /** + * Set the percentage output. + * + * @param percentOutput percent out for the motor controller. + */ + @Override + public void set(double percentOutput) { + motor.set(percentOutput); + } + + /** + * Set the voltage of the motor. + * + * @param voltage Voltage to set. + */ + @Override + public void setVoltage(double voltage) { + motor.setVoltage(voltage); + } + + /** + * Get the current output of the motor controller. + * + * @return The motor output as a double between -1 and 1. + */ + @Override + public double get() { + return motor.get(); + } + + /** + * Returns the inversion status of the motor. + * + *

This method checks the current motor output configuration and determines if the motor is set + * to be inverted. + * + * @return true if the motor is inverted (i.e., configured for counterclockwise positive + * rotation), false if the motor is not inverted (i.e., configured for clockwise positive + * rotation). + */ + @Override + public boolean getInverted() { + cfg.refresh(configuration.MotorOutput); + return (configuration.MotorOutput.Inverted == InvertedValue.CounterClockwise_Positive + ? true + : false); + } + + /** Disables the motor by calling the disable method on the underlying motor object. */ + @Override + public void disable() { + motor.disable(); + } + + /** Stop the motor by setting the output to 0. */ + @Override + public void stopMotor() { + motor.stopMotor(); + } + + /** + * Get the current output of the motor controller. + * + * @return Current output. + */ + @Override + public double getOutputCurrent() { + return motor.getTorqueCurrent().refresh().getValueAsDouble(); + } + + /** + * Sets the simulated instance of the motor for use in simulations. + * + * @param motorSimulationType The simulated instance of the motor. + */ + @Override + public void setMotorSimulationType(DCMotor motorSimulationType) { + motorSim = motorSimulationType; + } + + /** + * Update the motor simulation model with the current state of the motor. + * + * @param position The current angle of the motor in radians. + * @param velocity The current angular velocity of the motor in radians per second. + */ + @Override + public void simulationUpdate(Optional position, Double velocity) { + encoder.simulationUpdate(position, velocity); + } + + /** + * Sets the maximum angular velocity of the motor in rotations per minute. + * + * @param rpm The maximum angular velocity to set, represented as an AngularVelocity unit. + */ + @Override + public void setMaxRPM(AngularVelocity rpm) { + maxRPM = rpm; + } + + public void sendControlRequest(ControlRequest request) { + motor.setControl(request); + } + + /** + * Returns the configuration of the motor as a Motor object. + * + * @return The motor configuration + */ + @Override + public Motor getMotorConfig() { + return config; + } + + @Override + public SmartMotorController getSmartMotorController(SmartMotorControllerConfig config) { + return new TalonFXSWrapper(motor, motorSim, config); + } +} diff --git a/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java b/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java index b1b7b02a..32962486 100644 --- a/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java +++ b/src/main/java/org/frc5010/common/motors/hardware/GenericThriftyNovaMotor.java @@ -6,8 +6,8 @@ import static edu.wpi.first.units.Units.Amps; -import com.thethriftybot.ThriftyNova; -import com.thethriftybot.ThriftyNova.CurrentType; +import com.thethriftybot.devices.ThriftyNova; +import com.thethriftybot.devices.ThriftyNova.CurrentType; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.units.measure.AngularVelocity; import edu.wpi.first.units.measure.Current; @@ -46,6 +46,10 @@ public class GenericThriftyNovaMotor implements GenericMotorController { /** The configuration */ protected Motor config; + public GenericThriftyNovaMotor(int canId) { + motor = new ThriftyNova(canId); + } + private GenericThriftyNovaMotor(int canId, Motor config, Current currentLimit) { this(canId, config); setCurrentLimit(currentLimit); diff --git a/src/main/java/org/frc5010/common/sensors/ThriftyLimit.java b/src/main/java/org/frc5010/common/sensors/ThriftyLimit.java index 63fb3c64..4bfcea5b 100644 --- a/src/main/java/org/frc5010/common/sensors/ThriftyLimit.java +++ b/src/main/java/org/frc5010/common/sensors/ThriftyLimit.java @@ -4,7 +4,7 @@ package org.frc5010.common.sensors; -import com.thethriftybot.ThriftyNova; +import com.thethriftybot.devices.ThriftyNova; import java.util.function.BooleanSupplier; /** Add your docs here. */ diff --git a/src/main/java/org/frc5010/common/sensors/camera/GenericCamera.java b/src/main/java/org/frc5010/common/sensors/camera/GenericCamera.java index 5ab13327..e1efcd78 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/GenericCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/GenericCamera.java @@ -30,6 +30,8 @@ public abstract class GenericCamera implements PoseProvider { protected int colIndex; /** The name of the camera */ protected String name; + /** Whether or not to view the game pieces in simulation */ + protected boolean canViewGamePieces = true; /** * Create a new camera @@ -147,6 +149,24 @@ public double getTargetPitch() { */ public abstract double getTargetArea(); + /** + * Returns whether or not the camera can view game pieces in simulation mode. + * + * @return whether or not the camera can view game pieces in simulation mode + */ + public boolean canViewGamePieces() { + return canViewGamePieces; + } + + /** + * Sets whether or not the camera should view game pieces in simulation mode. + * + * @param canViewGamePieces whether or not the camera should view game pieces in simulation mode + */ + public void setCanViewGamePieces(boolean canViewGamePieces) { + this.canViewGamePieces = canViewGamePieces; + } + /** * A method to get the distance to the target. * diff --git a/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionFiducialTargetCamera.java b/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionFiducialTargetCamera.java index 6d2d8ec2..9c3005d2 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionFiducialTargetCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionFiducialTargetCamera.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; -import org.photonvision.PhotonPoseEstimator.PoseStrategy; /** A camera using the PhotonVision library. */ public class PhotonVisionFiducialTargetCamera extends PhotonVisionPoseCamera { @@ -32,11 +31,10 @@ public PhotonVisionFiducialTargetCamera( String name, int colIndex, AprilTagFieldLayout fieldLayout, - PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier, List fiducialIds) { - super(name, colIndex, fieldLayout, strategy, cameraToRobot, poseSupplier, fiducialIds); + super(name, colIndex, fieldLayout, cameraToRobot, poseSupplier, fiducialIds); this.fieldLayout = fieldLayout; targetFiducialIds = fiducialIds; } diff --git a/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionPoseCamera.java b/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionPoseCamera.java index 32c02f72..502b78bc 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionPoseCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/PhotonVisionPoseCamera.java @@ -4,6 +4,7 @@ package org.frc5010.common.sensors.camera; +import edu.wpi.first.apriltag.AprilTag; import edu.wpi.first.apriltag.AprilTagFieldLayout; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.VecBuilder; @@ -12,6 +13,7 @@ import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.RobotState; import edu.wpi.first.wpilibj.Timer; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; @@ -24,15 +26,12 @@ import org.frc5010.common.vision.VisionConstants; import org.photonvision.EstimatedRobotPose; import org.photonvision.PhotonPoseEstimator; -import org.photonvision.PhotonPoseEstimator.PoseStrategy; import org.photonvision.targeting.PhotonPipelineResult; /** A camera using the PhotonVision library. */ public class PhotonVisionPoseCamera extends PhotonVisionCamera implements FiducialTargetCamera { /** The pose estimator */ protected PhotonPoseEstimator poseEstimator; - /** The pose strategy */ - protected PoseStrategy strategy; /** The pose supplier */ protected Supplier poseSupplier; /** The current list of fiducial IDs */ @@ -52,37 +51,33 @@ public PhotonVisionPoseCamera( String name, int colIndex, AprilTagFieldLayout fieldLayout, - PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier) { super(name, colIndex, cameraToRobot); - this.strategy = strategy; this.poseSupplier = poseSupplier; this.fieldLayout = fieldLayout; - poseEstimator = new PhotonPoseEstimator(fieldLayout, strategy, cameraToRobot); - poseEstimator.setMultiTagFallbackStrategy(PoseStrategy.PNP_DISTANCE_TRIG_SOLVE); - visionLayout.addString( - "Primary Strategy " + name, () -> poseEstimator.getPrimaryStrategy().name()); + poseEstimator = new PhotonPoseEstimator(fieldLayout, cameraToRobot); } public PhotonVisionPoseCamera( String name, int colIndex, AprilTagFieldLayout fieldLayout, - PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier, List fiducialIds) { super(name, colIndex, cameraToRobot); - this.strategy = strategy; this.poseSupplier = poseSupplier; this.fieldLayout = fieldLayout; + this.fiducialIds = fiducialIds; visionLayout.addDouble("Observations", () -> input.poseObservations.length); - poseEstimator = new PhotonPoseEstimator(fieldLayout, strategy, cameraToRobot); - poseEstimator.setMultiTagFallbackStrategy(PoseStrategy.PNP_DISTANCE_TRIG_SOLVE); - visionLayout.addString( - "Primary Strategy " + name, () -> poseEstimator.getPrimaryStrategy().name()); + List filteredTags = + fieldLayout.getTags().stream().filter(tag -> fiducialIds.contains(tag.ID)).toList(); + AprilTagFieldLayout filteredLayout = + new AprilTagFieldLayout( + filteredTags, fieldLayout.getFieldLength(), fieldLayout.getFieldWidth()); + poseEstimator = new PhotonPoseEstimator(filteredLayout, cameraToRobot); } /** Update the camera and target with the latest result */ @@ -91,13 +86,28 @@ public void updateCameraInfo() { poseEstimator.addHeadingData(Timer.getFPGATimestamp(), poseSupplier.get().getRotation()); List observations = new ArrayList<>(); + SmartDashboard.putBoolean("Camera/" + name() + "/updating", true); super.updateCameraInfo(); Set tagIds = new HashSet<>(); for (PhotonPipelineResult iCamResult : camResults) { - Optional estimate = poseEstimator.update(iCamResult); + SmartDashboard.putBoolean("Camera/" + name() + "/resuls", iCamResult.hasTargets()); + Optional estimate = poseEstimator.estimateCoprocMultiTagPose(iCamResult); + + if (estimate.isEmpty() && !DriverStation.isDisabled()) { + estimate = poseEstimator.estimatePnpDistanceTrigSolvePose(iCamResult); + } + if (estimate.isPresent()) { + // if (!DriverStation.isDisabled()) { + // Optional finalEstimate = + // poseEstimator.estimatePnpDistanceTrigSolvePose(iCamResult); + // if (finalEstimate.isPresent()) { + // estimate = finalEstimate; + // } + // } + EstimatedRobotPose estimatedRobotPose = estimate.get(); Pose3d robotPose = estimatedRobotPose.estimatedPose; @@ -127,6 +137,13 @@ public void updateCameraInfo() { robotPose.getY(), robotPose.getRotation().toRotation2d().getDegrees() }); + SmartDashboard.putNumberArray( + "Camera/" + name() + "/Photon Camera " + name + " Robot Offset", + new double[] { + robotToCamera.getX(), + robotToCamera.getY(), + robotToCamera.getRotation().toRotation2d().getDegrees() + }); observations.add( new PoseObservation( @@ -156,7 +173,7 @@ public void updateCameraInfo() { @Override public Matrix getStdDeviations(PoseObservation observation) { - double stdDevFactor = Math.pow(observation.averageTagDistance(), 4.0) / observation.tagCount(); + double stdDevFactor = Math.pow(observation.averageTagDistance(), 2.0) / observation.tagCount(); double linearStdDev = VisionConstants.linearStdDevBaseline * stdDevFactor; double angularStdDev = VisionConstants.angularStdDevBaseline * stdDevFactor; diff --git a/src/main/java/org/frc5010/common/sensors/camera/QuestNavInterface.java b/src/main/java/org/frc5010/common/sensors/camera/QuestNavInterface.java new file mode 100644 index 00000000..7233a47f --- /dev/null +++ b/src/main/java/org/frc5010/common/sensors/camera/QuestNavInterface.java @@ -0,0 +1,331 @@ +package org.frc5010.common.sensors.camera; + +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.VecBuilder; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Translation3d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N3; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.RobotBase; +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.Trigger; +import gg.questnav.questnav.PoseFrame; +import gg.questnav.questnav.QuestNav; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import org.frc5010.common.drive.GenericDrivetrain; +import org.frc5010.common.drive.pose.PoseProvider; + +/** Add your docs here. */ +public class QuestNavInterface implements PoseProvider { + + private String networkTableRoot = "questnav"; + private Supplier robotVelocity = null; + private Transform3d robotToQuest; + private QuestNav questNav; + private boolean initializedPosition = false; + private PoseFrame latestPoseFrame = null; + + private ChassisSpeeds velocity; + private Pose3d previousPose; + private double previousTime; + private static boolean hasHardReset = false; + private static boolean initialReset = false; + + // Soft reset state — applied in code without sending a command to the headset + private Transform3d softResetTransform = new Transform3d(); + + // I guess this works + private static Pose2d latestPoseState = null; + + private Translation2d _calculatedOffsetToRobotCenter = new Translation2d(); + private int _calculatedOffsetToRobotCenterCount = 0; + + public QuestNavInterface(Transform3d robotToQuest) { + super(); + this.robotToQuest = robotToQuest; + this.questNav = new QuestNav(); + } + + public QuestNavInterface(Transform3d robotToQuest, String networkTableRoot) { + super(); + this.robotToQuest = robotToQuest; + this.networkTableRoot = networkTableRoot; + this.questNav = new QuestNav(); + } + + private Pose3d getRobotPoseFromQuestPose(Pose3d questPose) { + return questPose.transformBy(robotToQuest.inverse()); + } + + private Pose3d getQuestPoseFromRobotPose(Pose3d robotPose) { + return robotPose.transformBy(robotToQuest); + } + + public void withRobotSpeedSupplier(Supplier robotSpeed) { + robotVelocity = robotSpeed; + } + + /** + * Returns the robot pose as computed directly from the QuestNav headset plus the robotToQuest + * transform, with no soft-reset offset applied. This is the "hard-reset" pose layer. + */ + private Pose3d getProcessedRobotPose() { + if (latestPoseFrame == null) { + return new Pose3d(); + } + return getRobotPoseFromQuestPose(latestPoseFrame.questPose3d()); + } + + public Optional getRobotPose() { + if (RobotBase.isReal()) { + if (latestPoseFrame == null) { + return Optional.empty(); + } + return Optional.of(getProcessedRobotPose().transformBy(softResetTransform)); + } else { + return Optional.empty(); + } + } + + public Rotation3d getRotation() { + if (latestPoseFrame == null) { + return new Rotation3d(); + } + return getProcessedRobotPose().transformBy(softResetTransform).getRotation(); + } + + public Translation3d getPosition() { + if (latestPoseFrame == null) { + return new Translation3d(); + } + return getProcessedRobotPose().transformBy(softResetTransform).getTranslation(); + } + + private void updateObservations() { + PoseFrame[] unreadQuestFrames = questNav.getAllUnreadPoseFrames(); + if (unreadQuestFrames.length > 0) { + latestPoseFrame = unreadQuestFrames[unreadQuestFrames.length - 1]; + } + + List observations = new ArrayList<>(); + + if (initializedPosition) { + for (PoseFrame frame : unreadQuestFrames) { + Pose3d robotPose = + getRobotPoseFromQuestPose(frame.questPose3d()).transformBy(softResetTransform); + double captureTime = frame.dataTimestamp(); + observations.add( + new PoseObservation( + captureTime, + robotPose, + 0, + 0, + 0, + PoseObservationType.ENVIRONMENT_BASED, + ProviderType.ENVIRONMENT_BASED)); + } + } + input.connected = isActive(); + // Save pose observations to inputs object + input.poseObservations = new PoseObservation[observations.size()]; + for (int i = 0; i < observations.size(); i++) { + input.poseObservations[i] = observations.get(i); + } + } + + @Override + public Matrix getStdDeviations(PoseObservation observation) { + double calib = getConfidence(); + if (DriverStation.isDisabled()) { + calib = 10000; + } + if (null != robotVelocity) { + Translation2d questVelVector = + new Translation2d(getVelocity().vxMetersPerSecond, getVelocity().vyMetersPerSecond); + Translation2d robotVelVector = + new Translation2d( + robotVelocity.get().vxMetersPerSecond, robotVelocity.get().vyMetersPerSecond); + if (Math.abs(questVelVector.getNorm() - robotVelVector.getNorm()) > 1.0) { + calib = 10; + } + } + return VecBuilder.fill(calib, calib, calib * 0.2); + } + + public double getConfidence() { + if (RobotBase.isReal()) { + return 0.05; + } else { + return Double.MAX_VALUE; + } + } + + @Override + public boolean isConnected() { + return questNav.isConnected(); + } + + public boolean isActive() { + boolean simulation = RobotBase.isSimulation(); + // boolean disabled = DriverStation.isDisabled(); + + boolean isActive = + questNav.isConnected() + && (questNav.getFrameCount().orElse(0) > 0 && !simulation) + && questNav.isTracking(); + + return isActive && initializedPosition; + } + + /** + * Performs a soft reset: recalculates the pose offset in code without sending any command to the + * Quest headset. This is instantaneous and avoids the delay of a hard reset command. The + * soft-reset transform is stored and applied on top of the raw headset pose in {@link + * #getRobotPose()}, {@link #getPosition()}, and {@link #getRotation()}. + */ + public void softReset(Pose3d pose) { + // Transform3d(from, to) gives exactly the relative transform needed: + // processed.transformBy(softResetTransform) == pose + softResetTransform = new Transform3d(getProcessedRobotPose(), pose); + initializedPosition = true; + } + + /** + * Performs a hard reset: sends {@code questNav.setPose()} to the headset so its internal + * coordinate origin is relocated. Intended to be called only once at the beginning of a match. + * Subsequent resets during the match should use {@link #softReset(Pose3d)}. + */ + public void hardReset(Pose3d pose) { + if (isConnected()) { + Pose3d questPose = getQuestPoseFromRobotPose(pose); + questNav.setPose(questPose); + // Clear any accumulated soft-reset so the hard reset is authoritative + softResetTransform = new Transform3d(); + initializedPosition = true; + hasHardReset = initialReset; + initialReset = true; + } + } + + public static Trigger hasHardReset() { + return new Trigger(() -> hasHardReset); + } + + @Override + public void resetPose(Pose3d pose) { + if (isConnected()) { + SmartDashboard.putBoolean(networkTableRoot + "/Reset Pose", true); + softReset(pose); + } + } + + @Override + public ProviderType getType() { + return ProviderType.ENVIRONMENT_BASED; + } + + public int fiducialId() { + return 0; + } + + private void updateVelocity() { + return; // Implement if needed. + } + + public ChassisSpeeds getVelocity() { + if (null != velocity) { + return velocity; + } + return new ChassisSpeeds(); + } + + @Override + public void update() { + if (RobotBase.isReal()) { + questNav.commandPeriodic(); + updateVelocity(); + updateObservations(); + SmartDashboard.putBoolean(networkTableRoot + "/Reset Pose", false); + SmartDashboard.putBoolean("QUEST Connected", isConnected()); + SmartDashboard.putBoolean("QUEST Active", isActive()); + + Pose2d currPose = getRobotPose().orElse(new Pose3d()).toPose2d(); + latestPoseState = currPose; + SmartDashboard.putNumberArray( + networkTableRoot + "/Quest POSE Update", + new double[] {currPose.getX(), currPose.getY(), currPose.getRotation().getDegrees()}); + + ChassisSpeeds velocity = getVelocity(); + SmartDashboard.putNumberArray( + networkTableRoot + "/Velocity", + new double[] { + velocity.vxMetersPerSecond, velocity.vyMetersPerSecond, velocity.omegaRadiansPerSecond + }); + } + logInput(networkTableRoot); + } + + private Translation2d calculateOffsetToRobotCenter() { + if (null == latestPoseState) { + return new Translation2d(); + } + + Pose2d currentPose2d = latestPoseState; + + Rotation2d angle = currentPose2d.getRotation(); + Translation2d displacement = currentPose2d.getTranslation(); + + double x = + ((angle.getCos() - 1) * displacement.getX() + angle.getSin() * displacement.getY()) + / (2 * (1 - angle.getCos())); + double y = + ((-1 * angle.getSin()) * displacement.getX() + (angle.getCos() - 1) * displacement.getY()) + / (2 * (1 - angle.getCos())); + + return new Translation2d(x, y); + } + + public Command determineOffsetToRobotCenter(GenericDrivetrain drivetrain) { + return Commands.repeatingSequence( + Commands.run( + () -> { + SmartDashboard.putNumber("QUEST POSE", getPosition().getX()); + drivetrain.drive(new ChassisSpeeds(0, 0, 0.314)); + }, + drivetrain) + .withTimeout(3.0), + Commands.runOnce( + () -> { + // Update current offset + Translation2d offset = calculateOffsetToRobotCenter(); + + _calculatedOffsetToRobotCenter = + _calculatedOffsetToRobotCenter + .times( + (double) _calculatedOffsetToRobotCenterCount + / (_calculatedOffsetToRobotCenterCount + 1)) + .plus(offset.div(_calculatedOffsetToRobotCenterCount + 1)); + _calculatedOffsetToRobotCenterCount++; + + SmartDashboard.putNumberArray( + networkTableRoot + "/Quest Calculated Offset to Robot Center", + new double[] { + _calculatedOffsetToRobotCenter.getX(), _calculatedOffsetToRobotCenter.getY() + }); + })) + .beforeStarting( + Commands.runOnce(() -> resetPose(new Pose3d())).andThen(Commands.waitSeconds(1))); + } +} diff --git a/src/main/java/org/frc5010/common/sensors/camera/QuestNav.java b/src/main/java/org/frc5010/common/sensors/camera/QuestNavOld.java similarity index 98% rename from src/main/java/org/frc5010/common/sensors/camera/QuestNav.java rename to src/main/java/org/frc5010/common/sensors/camera/QuestNavOld.java index 8e753ad2..ca2a5a25 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/QuestNav.java +++ b/src/main/java/org/frc5010/common/sensors/camera/QuestNavOld.java @@ -43,7 +43,7 @@ import org.frc5010.common.drive.swerve.GenericSwerveDrivetrain; /** Add your docs here. */ -public class QuestNav implements PoseProvider { +public class QuestNavOld implements PoseProvider { private boolean initializedPosition = false; public static boolean isActive = false; private String networkTableRoot = "questnav"; @@ -100,14 +100,14 @@ public int getQuestRequest() { } } - public QuestNav(Transform3d robotToQuest) { + public QuestNavOld(Transform3d robotToQuest) { super(); this.robotToQuest = robotToQuest; setupNetworkTables(networkTableRoot); setupInitialTimestamp(); } - public QuestNav(Transform3d robotToQuest, String networkTableRoot) { + public QuestNavOld(Transform3d robotToQuest, String networkTableRoot) { super(); this.robotToQuest = robotToQuest; this.networkTableRoot = networkTableRoot; diff --git a/src/main/java/org/frc5010/common/sensors/camera/SimulatedCamera.java b/src/main/java/org/frc5010/common/sensors/camera/SimulatedCamera.java index 3220f160..ce786e76 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/SimulatedCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/SimulatedCamera.java @@ -48,13 +48,12 @@ public SimulatedCamera( String name, int colIndex, AprilTagFieldLayout fieldLayout, - PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier, int width, int height, double fov) { - super(name, colIndex, fieldLayout, strategy, cameraToRobot, poseSupplier); + super(name, colIndex, fieldLayout, cameraToRobot, poseSupplier); if (!tagsLoaded) { visionSim.addAprilTags(fieldLayout); tagsLoaded = true; @@ -131,7 +130,7 @@ public SimulatedCamera( PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier) { - this(name, colIndex, fieldLayout, strategy, cameraToRobot, poseSupplier, 640, 480, 70.0); + this(name, colIndex, fieldLayout, cameraToRobot, poseSupplier, 640, 480, 70.0); } /** Update the simulated camera */ diff --git a/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java b/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java index 89366727..68347485 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/SimulatedFiducialTargetCamera.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; -import org.photonvision.PhotonPoseEstimator.PoseStrategy; /** A simulated camera using the PhotonVision library. */ public class SimulatedFiducialTargetCamera extends SimulatedCamera { @@ -37,14 +36,13 @@ public SimulatedFiducialTargetCamera( String name, int colIndex, AprilTagFieldLayout fieldLayout, - PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier, List fiducialIds, int width, int height, double fov) { - super(name, colIndex, fieldLayout, strategy, cameraToRobot, poseSupplier, width, height, fov); + super(name, colIndex, fieldLayout, cameraToRobot, poseSupplier, width, height, fov); targetFiducialIds = fiducialIds; visionLayout.addDouble("Target ID", () -> target.map(it -> it.getFiducialId()).orElse(-1)); } diff --git a/src/main/java/org/frc5010/common/sensors/camera/SimulatedVisualTargetCamera.java b/src/main/java/org/frc5010/common/sensors/camera/SimulatedVisualTargetCamera.java index 3926fb8b..393574a3 100644 --- a/src/main/java/org/frc5010/common/sensors/camera/SimulatedVisualTargetCamera.java +++ b/src/main/java/org/frc5010/common/sensors/camera/SimulatedVisualTargetCamera.java @@ -9,7 +9,6 @@ import edu.wpi.first.math.geometry.Transform3d; import java.util.Optional; import java.util.function.Supplier; -import org.photonvision.PhotonPoseEstimator.PoseStrategy; /** A simulated camera using the PhotonVision library. */ public class SimulatedVisualTargetCamera extends SimulatedCamera { @@ -31,13 +30,12 @@ public SimulatedVisualTargetCamera( String name, int colIndex, AprilTagFieldLayout fieldLayout, - PoseStrategy strategy, Transform3d cameraToRobot, Supplier poseSupplier, int width, int height, double fov) { - super(name, colIndex, fieldLayout, strategy, cameraToRobot, poseSupplier, width, height, fov); + super(name, colIndex, fieldLayout, cameraToRobot, poseSupplier, width, height, fov); } /** Update the simulated camera */ diff --git a/src/main/java/org/frc5010/common/sensors/encoder/CanCoderEncoder.java b/src/main/java/org/frc5010/common/sensors/encoder/CanCoderEncoder.java index 26869438..c3d4c0d2 100644 --- a/src/main/java/org/frc5010/common/sensors/encoder/CanCoderEncoder.java +++ b/src/main/java/org/frc5010/common/sensors/encoder/CanCoderEncoder.java @@ -4,7 +4,6 @@ package org.frc5010.common.sensors.encoder; -import com.ctre.phoenix6.configs.CANcoderConfiguration; import com.ctre.phoenix6.hardware.CANcoder; import edu.wpi.first.math.util.Units; import java.util.Optional; @@ -21,7 +20,7 @@ public class CanCoderEncoder implements GenericEncoder { */ public CanCoderEncoder(int CanID) { this.canCoder = new CANcoder(CanID); - CANcoderConfiguration config = new CANcoderConfiguration(); + // CANcoderConfiguration config = new CANcoderConfiguration(); // config.absoluteSensorRange = AbsoluteSensorRange.Signed_PlusMinus180; // config.sensorCoefficient = 360.0 / 4096.0; // config.unitString = "degrees"; diff --git a/src/main/java/org/frc5010/common/sensors/encoder/RevEncoder.java b/src/main/java/org/frc5010/common/sensors/encoder/RevEncoder.java index 961ae08c..c7413c04 100644 --- a/src/main/java/org/frc5010/common/sensors/encoder/RevEncoder.java +++ b/src/main/java/org/frc5010/common/sensors/encoder/RevEncoder.java @@ -5,7 +5,7 @@ package org.frc5010.common.sensors.encoder; import com.revrobotics.RelativeEncoder; -import com.revrobotics.sim.SparkMaxSim; +import com.revrobotics.spark.SparkSim; import com.revrobotics.spark.config.EncoderConfig; import edu.wpi.first.wpilibj.RobotBase; import edu.wpi.first.wpilibj.simulation.RoboRioSim; @@ -14,7 +14,7 @@ public class RevEncoder implements GenericEncoder { protected RelativeEncoder encoder; /** The simulated instance of the motor */ - protected SparkMaxSim sparkMaxSim; + protected SparkSim sparkMaxSim; protected EncoderConfig config; protected double positionConversion = 1.0; @@ -25,7 +25,7 @@ public RevEncoder(RelativeEncoder encoder) { config = new EncoderConfig(); } - public void setSimulation(SparkMaxSim sparkMaxSim) { + public void setSimulation(SparkSim sparkMaxSim) { this.sparkMaxSim = sparkMaxSim; } diff --git a/src/main/java/org/frc5010/common/sensors/encoder/ThriftyNovaEncoder.java b/src/main/java/org/frc5010/common/sensors/encoder/ThriftyNovaEncoder.java index d0bae3c3..5b2aa862 100644 --- a/src/main/java/org/frc5010/common/sensors/encoder/ThriftyNovaEncoder.java +++ b/src/main/java/org/frc5010/common/sensors/encoder/ThriftyNovaEncoder.java @@ -4,11 +4,11 @@ package org.frc5010.common.sensors.encoder; -import com.thethriftybot.Conversion; -import com.thethriftybot.Conversion.PositionUnit; -import com.thethriftybot.Conversion.VelocityUnit; -import com.thethriftybot.ThriftyNova; -import com.thethriftybot.ThriftyNova.EncoderType; +import com.thethriftybot.devices.ThriftyNova; +import com.thethriftybot.devices.ThriftyNova.EncoderType; +import com.thethriftybot.util.Conversion; +import com.thethriftybot.util.Conversion.PositionUnit; +import com.thethriftybot.util.Conversion.VelocityUnit; import java.util.Optional; /** Add your docs here. */ diff --git a/src/main/java/org/frc5010/common/subsystems/CameraSystem.java b/src/main/java/org/frc5010/common/subsystems/CameraSystem.java index 9954143d..2e62c114 100644 --- a/src/main/java/org/frc5010/common/subsystems/CameraSystem.java +++ b/src/main/java/org/frc5010/common/subsystems/CameraSystem.java @@ -13,25 +13,56 @@ import org.frc5010.common.sensors.camera.GenericCamera; import org.frc5010.common.sensors.camera.SimulatedCamera; import org.frc5010.common.telemetry.DisplayBoolean; -import org.ironmaple.simulation.SimulatedArena; import org.photonvision.estimation.TargetModel; import org.photonvision.simulation.VisionTargetSim; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; /** - * This class is an abstract class that needs to be implemented by any subclass of CameraSystem. It - * is responsible for updating the camera information. + * Abstract base class for vision-based camera subsystems. + * + *

This class provides the framework for implementing camera subsystems that process vision data + * from one or more cameras. Subclasses implement specific behavior based on what the camera is + * tracking (AprilTags, targets, game pieces, etc.). + * + *

Key responsibilities: + * + *

    + *
  • Periodically update camera data ({@link #periodic()}) + *
  • Detect and track targets ({@link #hasValidTarget()}) + *
  • Calculate distance and orientation to targets ({@link #getDistanceToTarget()}) + *
  • Simulate vision targets in simulation mode ({@link #simulationPeriodic()}) + *
+ * + *

This is an abstract class; concrete implementations include: + * + *

    + *
  • {@link FiducialTargetSystem} - For AprilTag tracking + *
  • {@link VisibleTargetSystem} - For visual target tracking + *
  • {@link AprilTagPoseSystem} - For pose estimation + *
+ * + * @see GenericCamera + * @see GenericSubsystem */ public abstract class CameraSystem extends GenericSubsystem { - /** The camera object. */ + /** The camera instance providing the vision data */ protected GenericCamera camera; + /** Telemetry display for camera target validity state */ protected DisplayBoolean HAS_VALID_TARGET; + /** Target model used for vision simulation (default: 12-inch diameter circle at ~0.3556m) */ protected TargetModel targetModel = new TargetModel(0.3556); + /** Whether to view game pieces in simulation */ + protected boolean viewGamePieces = true; /** - * Creates a new CameraSystem. + * Creates a new CameraSystem with the specified camera. * - * @param camera the camera object + *

Initializes the camera subsystem and sets up telemetry displays for vision system status and + * target information. + * + * @param camera the {@link GenericCamera} instance to use for vision processing + * @throws NullPointerException if camera is null */ public CameraSystem(GenericCamera camera) { this.camera = camera; @@ -39,7 +70,10 @@ public CameraSystem(GenericCamera camera) { } /** - * This method is called once per scheduler run and it calls the updateCameraInfo() method. + * Periodic update method called by the WPILib scheduler. + * + *

This method is invoked once per scheduler cycle (~20ms on FRC robots). It updates camera + * data and processes vision information. * * @see #updateCameraInfo() */ @@ -51,6 +85,10 @@ public void periodic() { @Override public void simulationPeriodic() { + if (!camera.canViewGamePieces()) { + return; + } + // Update game piece A targets in simulation List gpas = SimulatedArena.getInstance().getGamePiecesByType(Constants.Simulation.gamePieceA).stream() .map(it -> it.getPose3d()) @@ -60,6 +98,7 @@ public void simulationPeriodic() { VisionTargetSim simTarget = new VisionTargetSim(gpa, targetModel); SimulatedCamera.visionSim.addVisionTargets("GPA", simTarget); } + // Update game piece B targets in simulation List gpbs = SimulatedArena.getInstance().getGamePiecesByType(Constants.Simulation.gamePieceB).stream() .map(it -> it.getPose3d()) @@ -72,27 +111,69 @@ public void simulationPeriodic() { } /** - * This is an abstract method that needs to be implemented by any subclass of CameraSystem. It is - * responsible for updating the camera information. + * Enables or disables the camera from viewing game pieces in simulation. When enabled, the camera + * will simulate vision targets for game pieces in the arena. This can be useful for testing + * vision code without a real camera. When disabled, the camera will only process vision + * information for real camera images. + * + * @param viewGamePieces whether to enable or disable viewing game pieces in simulation + */ + public void setViewGamePieces(boolean viewGamePieces) { + this.viewGamePieces = viewGamePieces; + } + + /** + * Updates camera information during each periodic cycle. + * + *

This method is called from {@link #periodic()} and is responsible for refreshing vision data + * from the camera. Subclasses may override this to perform additional processing beyond basic + * camera updates. + * + * @see GenericCamera#update() */ public void updateCameraInfo() { camera.update(); } /** - * Get the distance to the target - need to be implemented by any subclass of CameraSystem + * Gets the distance from the camera to the detected target. + * + *

This method must be implemented by subclasses to calculate the distance based on the + * specific type of target being tracked. The meaning of "distance" depends on the implementation: + * + *

    + *
  • For target-based systems: horizontal distance to the target + *
  • For AprilTag systems: 3D distance to the tag center + *
  • For game piece systems: distance to the piece + *
* - * @return the distance to the target + * @return the distance to the target in meters, or a negative value if no valid target */ public abstract double getDistanceToTarget(); /** - * Does the camera have a valid target? + * Checks if the camera currently has a valid target in view. * - * @return true if the camera has a valid target + *

This abstract method must be implemented by subclasses to determine the validity of the + * current target based on confidence, ambiguity, or other criteria appropriate to the specific + * vision task. + * + * @return {@code true} if a valid target is detected, {@code false} otherwise */ protected abstract boolean hasValidTarget(); + /** + * Creates a Trigger that activates when a valid target is detected. + * + *

This can be used in command scheduling to automatically trigger actions when a target + * becomes available. For example: + * + *

+   *   cameraSystem.hasAValidTarget().onTrue(alignCommand);
+   * 
+ * + * @return a {@link Trigger} that is active when {@link #hasValidTarget()} returns true + */ public Trigger hasAValidTarget() { return new Trigger(this::hasValidTarget); } diff --git a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java index eeb4f1ba..1fce3083 100644 --- a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java +++ b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java @@ -171,7 +171,7 @@ private LEDStrip(int kPort, int length) { * Sets the default pattern to run on the entire LED strip when the LED subsystem is not running a * command. * - * @param defaultPattern the LED pattern to run by default + * @param pattern the LED pattern to run by default */ public void setFullPattern(LEDPattern pattern) { this.defaultPattern = pattern; @@ -189,7 +189,7 @@ public int getLength() { /** * Creates a command that runs a pattern on the entire LED strip. * - * @param defaultPattern the LED pattern to run + * @return a command that runs the pattern */ public Command runPattern() { return run( diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayAngle.java b/src/main/java/org/frc5010/common/telemetry/DisplayAngle.java index 9c57bb98..051d6ae9 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayAngle.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayAngle.java @@ -83,7 +83,7 @@ public DisplayAngle(final Angle angle, final String name, final String table) { * @param angle - angle with units * @param name - name of the angle * @param table - name of the table - * @param debug - whether or not to debug + * @param logLevel - log level */ public DisplayAngle(final Angle angle, final String name, final String table, LogLevel logLevel) { super(String.format("%s (%s)", name, angle.unit().symbol()), table, logLevel); diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayCurrent.java b/src/main/java/org/frc5010/common/telemetry/DisplayCurrent.java index b118ef00..fed0d609 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayCurrent.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayCurrent.java @@ -31,8 +31,8 @@ public class DisplayCurrent extends DisplayableValue { /** * Add a current to the dashboard * - * @param unit - current unit - * @param unitLength - current in that unit + * @param current - current unit + * @param unit - the current unit * @param name - name of the variable * @param table - name of the table */ @@ -44,11 +44,11 @@ public DisplayCurrent( /** * Add a current to the dashboard * - * @param unit - current unit - * @param unitLength - current in that unit + * @param current - current unit + * @param unit - the current unit * @param name - name of the variable * @param table - name of the table - * @param debug - debug mode + * @param logLevel - log level */ public DisplayCurrent( final double current, @@ -70,7 +70,7 @@ public DisplayCurrent( /** * Add a current to the dashboard * - * @param unit - current with units + * @param current - current with units * @param name - name of the variable * @param table - name of the table */ @@ -81,10 +81,10 @@ public DisplayCurrent(final Current current, final String name, final String tab /** * Add a current to the dashboard * - * @param unit - current with units + * @param current - current with units * @param name - name of the variable * @param table - name of the table - * @param debug - debug mode + * @param logLevel - log level */ public DisplayCurrent( final Current current, final String name, final String table, LogLevel logLevel) { @@ -118,11 +118,12 @@ protected void init(LogLevel logLevel) { } // Setters + /** - * Sets the current + * Sets the current to the dashboard * + * @param current - current in that unit * @param unit - current unit - * @param unitLength - current in that unit */ public void setCurrent(final double current, final CurrentUnit unit) { setCurrent(current, unit, true); diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayDouble.java b/src/main/java/org/frc5010/common/telemetry/DisplayDouble.java index 59c3f81f..74f773c6 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayDouble.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayDouble.java @@ -39,7 +39,7 @@ public DisplayDouble(final double defaultValue, final String name, final String * @param defaultValue the default value * @param name the name * @param table the table - * @param debug debug + * @param logLevel the log level */ public DisplayDouble( final double defaultValue, final String name, final String table, final LogLevel logLevel) { diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayFloat.java b/src/main/java/org/frc5010/common/telemetry/DisplayFloat.java index 14270d6a..e803a97d 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayFloat.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayFloat.java @@ -39,7 +39,7 @@ public DisplayFloat(final float defaultValue, final String name, final String ta * @param defaultValue the default value * @param name the name of the variable * @param table the name of the table - * @param debug the debug mode + * @param logLevel the log level */ public DisplayFloat( final float defaultValue, final String name, final String table, final LogLevel logLevel) { diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayLength.java b/src/main/java/org/frc5010/common/telemetry/DisplayLength.java index 5485c04b..97c1e49a 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayLength.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayLength.java @@ -31,8 +31,8 @@ public class DisplayLength extends DisplayableValue { /** * Add a length to the dashboard * - * @param unit - length unit - * @param unitLength - length in that unit + * @param length - length + * @param unit - unit * @param name - name of the variable * @param table - name of the table */ @@ -44,11 +44,11 @@ public DisplayLength( /** * Add a length to the dashboard * - * @param unit - length unit - * @param unitLength - length in that unit + * @param length - length unit in the unit + * @param unit - unit * @param name - name of the variable * @param table - name of the table - * @param debug - debug mode + * @param logLevel - the log level */ public DisplayLength( final double length, @@ -70,7 +70,7 @@ public DisplayLength( /** * Add a length to the dashboard * - * @param unit - length with units + * @param length - length with units * @param name - name of the variable * @param table - name of the table */ @@ -81,9 +81,10 @@ public DisplayLength(final Distance length, final String name, final String tabl /** * Add a length to the dashboard * - * @param unit - length with units + * @param length - length with units * @param name - name of the variable * @param table - name of the table + * @param logLevel - the log level */ public DisplayLength( final Distance length, final String name, final String table, LogLevel logLevel) { @@ -117,22 +118,23 @@ protected void init(LogLevel logLevel) { } // Setters + /** - * Sets the length + * Sets the length using a value and a unit, and publishes the value * - * @param unit - length unit - * @param unitLength - length in that unit + * @param length the value of the length to set + * @param unit the unit of the length to set */ public void setLength(final double length, final DistanceUnit unit) { setLength(length, unit, true); } /** - * Sets the length + * Sets the length using a value and a unit, and optionally publishes the value * - * @param unit - length unit - * @param unitLength - length in that unit - * @param publish - publish the value + * @param length the value of the length to set + * @param unit the unit of the length to set + * @param publish whether or not to publish the value */ public void setLength(final double length, final DistanceUnit unit, final boolean publish) { setLength(unit.of(length), publish); diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayLong.java b/src/main/java/org/frc5010/common/telemetry/DisplayLong.java index b3698aa5..226afef5 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayLong.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayLong.java @@ -39,7 +39,7 @@ public DisplayLong(final long defaultValue, final String name, final String tabl * @param defaultValue the default value * @param name the name * @param table the table - * @param debug which log level to dipslay at + * @param logLevel the log level */ public DisplayLong( final long defaultValue, final String name, final String table, final LogLevel logLevel) { diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayString.java b/src/main/java/org/frc5010/common/telemetry/DisplayString.java index 54df28f5..82ea198f 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayString.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayString.java @@ -39,7 +39,7 @@ public DisplayString(final String defaultValue, final String name, final String * @param defaultValue the default value * @param name the name of the variable * @param table the name of the table - * @param debug the debug mode + * @param logLevel the log level */ public DisplayString( final String defaultValue, final String name, final String table, final LogLevel logLevel) { diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayTime.java b/src/main/java/org/frc5010/common/telemetry/DisplayTime.java index bf1542bd..f8ffc77d 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayTime.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayTime.java @@ -48,7 +48,7 @@ public DisplayTime( * @param unitTime - time in that unit * @param name - name of the time * @param table - name of the table - * @param debug - debug mode + * @param logLevel - the log level */ public DisplayTime( final double unitTime, @@ -69,7 +69,6 @@ public DisplayTime( /** * Add a time to the dashboard * - * @param unit - time unit * @param unitTime - time in that unit * @param name - name of the time * @param table - name of the table @@ -81,11 +80,10 @@ public DisplayTime(final Time unitTime, final String name, final String table) { /** * Add a time to the dashboard * - * @param unit - time unit - * @param unitTime - time in that unit - * @param name - name of the time - * @param table - name of the table - * @param debug - debug mode + * @param unitTime + * @param name + * @param table + * @param logLevel */ public DisplayTime( final Time unitTime, final String name, final String table, final LogLevel logLevel) { diff --git a/src/main/java/org/frc5010/common/telemetry/DisplayVoltage.java b/src/main/java/org/frc5010/common/telemetry/DisplayVoltage.java index 3d637d96..97c9f659 100644 --- a/src/main/java/org/frc5010/common/telemetry/DisplayVoltage.java +++ b/src/main/java/org/frc5010/common/telemetry/DisplayVoltage.java @@ -30,10 +30,10 @@ public class DisplayVoltage extends DisplayableValue { /** * Add a voltage to the dashboard * - * @param unit - voltage unit - * @param unitLength - voltage in that unit - * @param name - name of the variable - * @param table - name of the table + * @param voltage + * @param unit + * @param name + * @param table */ public DisplayVoltage( final double voltage, final VoltageUnit unit, final String name, final String table) { @@ -43,11 +43,11 @@ public DisplayVoltage( /** * Add a voltage to the dashboard * - * @param unit - voltage unit - * @param unitLength - voltage in that unit - * @param name - name of the variable - * @param table - name of the table - * @param debug - debug mode + * @param voltage + * @param unit + * @param name + * @param table + * @param logLevel */ public DisplayVoltage( final double voltage, @@ -69,9 +69,9 @@ public DisplayVoltage( /** * Add a voltage to the dashboard * - * @param unit - voltage with units - * @param name - name of the variable - * @param table - name of the table + * @param voltage + * @param name + * @param table */ public DisplayVoltage(final Voltage voltage, final String name, final String table) { this(voltage, name, table, LogLevel.COMPETITION); @@ -80,10 +80,10 @@ public DisplayVoltage(final Voltage voltage, final String name, final String tab /** * Add a voltage to the dashboard * - * @param unit - voltage with units - * @param name - name of the variable - * @param table - name of the table - * @param debug - debug mode + * @param voltage + * @param name + * @param table + * @param logLevel */ public DisplayVoltage( final Voltage voltage, final String name, final String table, LogLevel logLevel) { @@ -117,22 +117,23 @@ protected void init(LogLevel logLevel) { } // Setters + /** - * Sets the voltage + * Sets the voltage using a value and a unit, and publishes the value to the dashboard. * - * @param unit - voltage unit - * @param unitLength - voltage in that unit + * @param voltage the value of the voltage to set + * @param unit the unit of the voltage to set */ public void setVoltage(final double voltage, final VoltageUnit unit) { setVoltage(voltage, unit, true); } /** - * Sets the voltage + * Sets the voltage using a value and a unit, and optionally publishes the value * - * @param unit - voltage unit - * @param unitLength - voltage in that unit - * @param publish - publish the value + * @param voltage the value of the voltage to set + * @param unit the unit of the voltage to set + * @param publish whether to publish the value after setting it */ public void setVoltage(final double voltage, final VoltageUnit unit, final boolean publish) { setVoltage(unit.of(voltage), publish); diff --git a/src/main/java/org/frc5010/common/utils/geometry/AllianceFlipUtil.java b/src/main/java/org/frc5010/common/utils/geometry/AllianceFlipUtil.java new file mode 100644 index 00000000..923b4302 --- /dev/null +++ b/src/main/java/org/frc5010/common/utils/geometry/AllianceFlipUtil.java @@ -0,0 +1,62 @@ +// Copyright (c) 2025-2026 Littleton Robotics +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package org.frc5010.common.utils.geometry; + +import edu.wpi.first.math.geometry.*; +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.wpilibj.DriverStation; + +public class AllianceFlipUtil { + private static Distance fieldWidth; + private static Distance fieldLength; + + public static void configure(Distance fieldWidth, Distance fieldLength) { + AllianceFlipUtil.fieldWidth = fieldWidth; + AllianceFlipUtil.fieldLength = fieldLength; + } + + public static double applyX(double x) { + return shouldFlip() ? fieldLength.magnitude() - x : x; + } + + public static double applyY(double y) { + return shouldFlip() ? fieldWidth.magnitude() - y : y; + } + + public static Translation2d apply(Translation2d translation) { + return new Translation2d(applyX(translation.getX()), applyY(translation.getY())); + } + + public static Rotation2d apply(Rotation2d rotation) { + return shouldFlip() ? rotation.rotateBy(Rotation2d.kPi) : rotation; + } + + public static Pose2d apply(Pose2d pose) { + return shouldFlip() + ? new Pose2d(apply(pose.getTranslation()), apply(pose.getRotation())) + : pose; + } + + public static Translation3d apply(Translation3d translation) { + return new Translation3d( + applyX(translation.getX()), applyY(translation.getY()), translation.getZ()); + } + + public static Rotation3d apply(Rotation3d rotation) { + return shouldFlip() ? rotation.rotateBy(new Rotation3d(0.0, 0.0, Math.PI)) : rotation; + } + + public static Pose3d apply(Pose3d pose) { + return new Pose3d(apply(pose.getTranslation()), apply(pose.getRotation())); + } + + public static boolean shouldFlip() { + return DriverStation.getAlliance().isPresent() + && DriverStation.getAlliance().get() == DriverStation.Alliance.Red; + } +} diff --git a/src/main/java/org/frc5010/common/utils/geometry/GeomUtil.java b/src/main/java/org/frc5010/common/utils/geometry/GeomUtil.java new file mode 100644 index 00000000..8b6e678c --- /dev/null +++ b/src/main/java/org/frc5010/common/utils/geometry/GeomUtil.java @@ -0,0 +1,174 @@ +// Copyright (c) 2025-2026 Littleton Robotics +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +package org.frc5010.common.utils.geometry; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Transform2d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.math.geometry.Translation2d; +import edu.wpi.first.math.geometry.Twist2d; +import edu.wpi.first.math.kinematics.ChassisSpeeds; + +/** Geometry utilities for working with translations, rotations, transforms, and poses. */ +public class GeomUtil { + /** + * Creates a pure translating transform + * + * @param translation The translation to create the transform with + * @return The resulting transform + */ + public static Transform2d toTransform2d(Translation2d translation) { + return new Transform2d(translation, Rotation2d.kZero); + } + + /** + * Creates a pure translating transform + * + * @param x The x coordinate of the translation + * @param y The y coordinate of the translation + * @return The resulting transform + */ + public static Transform2d toTransform2d(double x, double y) { + return new Transform2d(x, y, Rotation2d.kZero); + } + + /** + * Creates a pure rotating transform + * + * @param rotation The rotation to create the transform with + * @return The resulting transform + */ + public static Transform2d toTransform2d(Rotation2d rotation) { + return new Transform2d(Translation2d.kZero, rotation); + } + + /** + * Converts a Pose2d to a Transform2d to be used in a kinematic chain + * + * @param pose The pose that will represent the transform + * @return The resulting transform + */ + public static Transform2d toTransform2d(Pose2d pose) { + return new Transform2d(pose.getTranslation(), pose.getRotation()); + } + + public static Pose2d inverse(Pose2d pose) { + Rotation2d rotationInverse = pose.getRotation().unaryMinus(); + return new Pose2d( + pose.getTranslation().unaryMinus().rotateBy(rotationInverse), rotationInverse); + } + + /** + * Converts a Transform2d to a Pose2d to be used as a position or as the start of a kinematic + * chain + * + * @param transform The transform that will represent the pose + * @return The resulting pose + */ + public static Pose2d toPose2d(Transform2d transform) { + return new Pose2d(transform.getTranslation(), transform.getRotation()); + } + + /** + * Creates a pure translated pose + * + * @param translation The translation to create the pose with + * @return The resulting pose + */ + public static Pose2d toPose2d(Translation2d translation) { + return new Pose2d(translation, Rotation2d.kZero); + } + + /** + * Creates a pure rotated pose + * + * @param rotation The rotation to create the pose with + * @return The resulting pose + */ + public static Pose2d toPose2d(Rotation2d rotation) { + return new Pose2d(Translation2d.kZero, rotation); + } + + /** + * Multiplies a twist by a scaling factor + * + * @param twist The twist to multiply + * @param factor The scaling factor for the twist components + * @return The new twist + */ + public static Twist2d multiply(Twist2d twist, double factor) { + return new Twist2d(twist.dx * factor, twist.dy * factor, twist.dtheta * factor); + } + + /** + * Converts a Pose3d to a Transform3d to be used in a kinematic chain + * + * @param pose The pose that will represent the transform + * @return The resulting transform + */ + public static Transform3d toTransform3d(Pose3d pose) { + return new Transform3d(pose.getTranslation(), pose.getRotation()); + } + + /** + * Converts a Transform3d to a Transform2d + * + * @param transform The original transform + * @return The resulting transform + */ + public static Transform2d toTransform2d(Transform3d transform) { + return new Transform2d( + transform.getTranslation().toTranslation2d(), transform.getRotation().toRotation2d()); + } + + /** + * Converts a Transform3d to a Pose3d to be used as a position or as the start of a kinematic + * chain + * + * @param transform The transform that will represent the pose + * @return The resulting pose + */ + public static Pose3d toPose3d(Transform3d transform) { + return new Pose3d(transform.getTranslation(), transform.getRotation()); + } + + /** + * Converts a ChassisSpeeds to a Twist2d by extracting two dimensions (Y and Z). chain + * + * @param speeds The original translation + * @return The resulting translation + */ + public static Twist2d toTwist2d(ChassisSpeeds speeds) { + return new Twist2d( + speeds.vxMetersPerSecond, speeds.vyMetersPerSecond, speeds.omegaRadiansPerSecond); + } + + /** + * Creates a new pose from an existing one using a different translation value. + * + * @param pose The original pose + * @param translation The new translation to use + * @return The new pose with the new translation and original rotation + */ + public static Pose2d withTranslation(Pose2d pose, Translation2d translation) { + return new Pose2d(translation, pose.getRotation()); + } + + /** + * Creates a new pose from an existing one using a different rotation value. + * + * @param pose The original pose + * @param rotation The new rotation to use + * @return The new pose with the original translation and new rotation + */ + public static Pose2d withRotation(Pose2d pose, Rotation2d rotation) { + return new Pose2d(pose.getTranslation(), rotation); + } +} diff --git a/src/main/java/org/frc5010/common/vision/AprilTags.java b/src/main/java/org/frc5010/common/vision/AprilTags.java index 95fb9a28..dd96abe0 100644 --- a/src/main/java/org/frc5010/common/vision/AprilTags.java +++ b/src/main/java/org/frc5010/common/vision/AprilTags.java @@ -17,36 +17,75 @@ import java.util.List; import java.util.Map; -/** Add your docs here. */ +/** + * AprilTag field layout and utilities for vision-based pose estimation. + * + *

This class provides access to AprilTag field layouts (both standard official layouts and + * custom layouts) used for vision-based localization on FRC robots. It maintains the mapping + * between AprilTag IDs and their poses on the field, which is essential for accurate pose + * estimation using vision. + * + *

The class supports: + * + *

    + *
  • Official FIRST game field layouts + *
  • Custom practice field layouts + *
  • Reverse lookup from poses to AprilTag IDs + *
+ * + *

Usage: + * + *

+ *   AprilTagFieldLayout layout = AprilTags.aprilTagFieldLayout;
+ *   Optional<Pose3d> tagPose = layout.getTagPose(5);
+ * 
+ * + * @see AprilTagFieldLayout + * @see AprilTag + */ public class AprilTags { - /** The default field layout */ + /** The official AprilTag field layout for the current FIRST game */ public static AprilTagFieldLayout aprilTagFieldLayout; - /** The default room layout */ + /** Alternative AprilTag layout for practice/lab fields */ public static AprilTagFieldLayout aprilTagRoomLayout; - /** A map of poses to April Tag IDs for reverse lookup */ + /** Reverse lookup map: given a pose, find the corresponding AprilTag ID */ public static Map poseToID = new HashMap<>(); - /** An set of enum constants that define the April Tag positions for a custom field layout. */ + /** + * Enumeration of custom AprilTag positions for FRC5010 practice field. + * + *

This enum defines the ID, position, and orientation of AprilTags on a custom practice field + * layout. Each constant represents one AprilTag with its coordinates (x, y, z in meters) and + * rotation angles. + * + *

The coordinate system is: + * + *

    + *
  • X-axis: forward direction along the field length + *
  • Y-axis: left/right across the field width + *
  • Z-axis: up from the floor + *
+ */ public static enum AprilTag5010 { - /** ID 0 */ + /** AprilTag ID 0 at custom position */ ID0(0, 0, 0, 0, 0), - /** ID 1 */ + /** AprilTag ID 1 at custom position */ ID1(0, 0, 0, 0, 0), - /** ID 2 */ + /** AprilTag ID 2 at custom position */ ID2(0, 0, 0, 0, 0), - /** ID 3 */ + /** AprilTag ID 3 at custom position */ ID3(0, 0, 0, 0, 0), - /** ID 4 */ + /** AprilTag ID 4 at custom position */ ID4(0, 0, 0, 0, 0), - /** ID 5 */ + /** AprilTag ID 5 at custom position */ ID5(0, 0, 0, 0, 0), - /** ID 6 */ + /** AprilTag ID 6 at custom position */ ID6(0, 0, 0, 0, 0), - /** ID 7 */ + /** AprilTag ID 7 at custom position */ ID7(0, 0, 0, 0, 0), - /** ID 8 */ + /** AprilTag ID 8 at custom position */ ID8(0, 0, 0, 0, 0), - /** ID 9 */ + /** AprilTag ID 9 at custom position */ ID9(0, 0, 0, 0, 0), /** ID 10 */ ID10(0, 0, 0, 0, 0), diff --git a/src/main/java/org/frc5010/common/vision/VisionConstants.java b/src/main/java/org/frc5010/common/vision/VisionConstants.java index f59b9ee6..b1888793 100644 --- a/src/main/java/org/frc5010/common/vision/VisionConstants.java +++ b/src/main/java/org/frc5010/common/vision/VisionConstants.java @@ -10,8 +10,8 @@ public class VisionConstants { public static String SBTabVisionDisplay = "Vision"; // Basic filtering thresholds - public static double maxAmbiguity = 0.3; - public static double maxZError = 10; + public static double maxAmbiguity = 0.15; + public static double maxZError = 1; // Standard deviation baselines, for 1 meter distance and 1 tag // (Adjusted automatically based on distance and # of tags) diff --git a/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnField.java b/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnField.java index 4e3d9b4c..d00e3f82 100644 --- a/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnField.java +++ b/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnField.java @@ -7,7 +7,7 @@ import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Translation2d; -import org.ironmaple.simulation.gamepieces.GamePieceOnFieldSimulation; +import swervelib.simulation.ironmaple.simulation.gamepieces.GamePieceOnFieldSimulation; /** Add your docs here. */ public class FRC5010BallOnField extends GamePieceOnFieldSimulation { diff --git a/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnTheFly.java b/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnTheFly.java index e35bfe90..3a656088 100644 --- a/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnTheFly.java +++ b/src/main/java/org/frc5010/lobbinloco/FRC5010BallOnTheFly.java @@ -11,7 +11,7 @@ import edu.wpi.first.units.measure.Angle; import edu.wpi.first.units.measure.Distance; import edu.wpi.first.units.measure.LinearVelocity; -import org.ironmaple.simulation.gamepieces.GamePieceProjectile; +import swervelib.simulation.ironmaple.simulation.gamepieces.GamePieceProjectile; /** Add your docs here. */ public class FRC5010BallOnTheFly extends GamePieceProjectile { diff --git a/src/main/java/org/frc5010/lobbinloco/LobbinLoco.java b/src/main/java/org/frc5010/lobbinloco/LobbinLoco.java index a2755732..6c6282b2 100644 --- a/src/main/java/org/frc5010/lobbinloco/LobbinLoco.java +++ b/src/main/java/org/frc5010/lobbinloco/LobbinLoco.java @@ -13,8 +13,8 @@ import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.units.Units; import org.dyn4j.geometry.Circle; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.gamepieces.GamePieceOnFieldSimulation.GamePieceInfo; +import swervelib.simulation.ironmaple.simulation.SimulatedArena; +import swervelib.simulation.ironmaple.simulation.gamepieces.GamePieceOnFieldSimulation.GamePieceInfo; /** Add your docs here. */ public class LobbinLoco extends SimulatedArena { diff --git a/src/main/java/org/frc5010/package-info.java b/src/main/java/org/frc5010/package-info.java index 2561b578..5c2c6a66 100644 --- a/src/main/java/org/frc5010/package-info.java +++ b/src/main/java/org/frc5010/package-info.java @@ -1,5 +1,5 @@ /** - * Yet-Another Generic Swerve Library (YAGSL) main package AKA swervelib. + * FRC5010Lib main package * * @version 1.0.0 */ diff --git a/src/main/java/swervelib/SwerveController.java b/src/main/java/swervelib/SwerveController.java deleted file mode 100644 index 809ade58..00000000 --- a/src/main/java/swervelib/SwerveController.java +++ /dev/null @@ -1,223 +0,0 @@ -package swervelib; - -import edu.wpi.first.math.controller.PIDController; -import edu.wpi.first.math.filter.SlewRateLimiter; -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import swervelib.parser.SwerveControllerConfiguration; - -/** Controller class used to convert raw inputs into robot speeds. */ -public class SwerveController { - - /** - * {@link SwerveControllerConfiguration} object storing data to generate the {@link PIDController} - * for controlling the robot heading, and deadband for heading joystick. - */ - public final SwerveControllerConfiguration config; - /** PID Controller for the robot heading. */ - public final PIDController thetaController; // TODO: Switch to ProfilePIDController - /** Last angle as a scalar [-1,1] the robot was set to. */ - public double lastAngleScalar; - /** {@link SlewRateLimiter} for movement in the X direction in meters/second. */ - public SlewRateLimiter xLimiter = null; - /** {@link SlewRateLimiter} for movement in the Y direction in meters/second. */ - public SlewRateLimiter yLimiter = null; - /** {@link SlewRateLimiter} for angular movement in radians/second. */ - public SlewRateLimiter angleLimiter = null; - - /** - * Construct the SwerveController object which is used for determining the speeds of the robot - * based on controller input. - * - * @param cfg {@link SwerveControllerConfiguration} containing the PIDF variables for the heading - * PIDF. - */ - public SwerveController(SwerveControllerConfiguration cfg) { - config = cfg; - thetaController = config.headingPIDF.createPIDController(); - thetaController.enableContinuousInput(-Math.PI, Math.PI); - lastAngleScalar = 0; - } - - /** - * Helper function to get the {@link Translation2d} of the chassis speeds given the {@link - * ChassisSpeeds}. - * - * @param speeds Chassis speeds. - * @return {@link Translation2d} of the speed the robot is going in. - */ - public static Translation2d getTranslation2d(ChassisSpeeds speeds) { - return new Translation2d(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond); - } - - /** - * Add slew rate limiters to all controls. This prevents the robot from ramping up too much. To - * disable a {@link SlewRateLimiter} set the desired one to null. - * - * @param x The {@link SlewRateLimiter} for the X velocity in meters/second. - * @param y The {@link SlewRateLimiter} for the Y velocity in meters/second. - * @param angle The {@link SlewRateLimiter} for the angular velocity in radians/second. - */ - public void addSlewRateLimiters(SlewRateLimiter x, SlewRateLimiter y, SlewRateLimiter angle) { - xLimiter = x; - yLimiter = y; - angleLimiter = angle; - } - - /** - * Calculate the hypot deadband and check if the joystick is within it. - * - * @param x The x value for the joystick in which the deadband should be applied. - * @param y The y value for the joystick in which the deadband should be applied. - * @return Whether the values are within the deadband from {@link - * SwerveControllerConfiguration#angleJoyStickRadiusDeadband}. - */ - public boolean withinHypotDeadband(double x, double y) { - return Math.hypot(x, y) < config.angleJoyStickRadiusDeadband; - } - - /** - * Get the chassis speeds based on controller input of 1 joystick [-1,1] and an angle. - * - * @param xInput X joystick input for the robot to move in the X direction. X = xInput * maxSpeed - * @param yInput Y joystick input for the robot to move in the Y direction. Y = yInput * maxSpeed; - * @param angle The desired angle of the robot in radians. - * @param currentHeadingAngleRadians The current robot heading in radians. - * @param maxSpeed Maximum speed in meters per second. - * @return {@link ChassisSpeeds} which can be sent to the Swerve Drive. - */ - public ChassisSpeeds getTargetSpeeds( - double xInput, - double yInput, - double angle, - double currentHeadingAngleRadians, - double maxSpeed) { - // Convert joystick inputs to m/s by scaling by max linear speed. Also uses a cubic function - // to allow for precise control and fast movement. - double x = xInput * maxSpeed; - double y = yInput * maxSpeed; - - return getRawTargetSpeeds(x, y, angle, currentHeadingAngleRadians); - } - - /** - * Get the angle in radians based off of the heading joysticks. - * - * @param headingX X joystick which controls the angle of the robot. - * @param headingY Y joystick which controls the angle of the robot. - * @return angle in radians from the joystick. - */ - public double getJoystickAngle(double headingX, double headingY) { - lastAngleScalar = - withinHypotDeadband(headingX, headingY) ? lastAngleScalar : Math.atan2(headingX, headingY); - return lastAngleScalar; - } - - /** - * Get the chassis speeds based on controller input of 2 joysticks. One for speeds in which - * direction. The other for the angle of the robot. - * - * @param xInput X joystick input for the robot to move in the X direction. - * @param yInput Y joystick input for the robot to move in the Y direction. - * @param headingX X joystick which controls the angle of the robot. - * @param headingY Y joystick which controls the angle of the robot. - * @param currentHeadingAngleRadians The current robot heading in radians. - * @param maxSpeed Maximum speed of the drive motors in meters per second, multiplier of the - * xInput and yInput. - * @return {@link ChassisSpeeds} which can be sent to the Swerve Drive. - */ - public ChassisSpeeds getTargetSpeeds( - double xInput, - double yInput, - double headingX, - double headingY, - double currentHeadingAngleRadians, - double maxSpeed) { - // Converts the horizontal and vertical components to the commanded angle, in radians, unless - // the joystick is near - // the center (i. e. has been released), in which case the angle is held at the last valid - // joystick input (hold - // position when stick released). - double angle = - withinHypotDeadband(headingX, headingY) ? lastAngleScalar : Math.atan2(headingX, headingY); - ChassisSpeeds speeds = - getTargetSpeeds(xInput, yInput, angle, currentHeadingAngleRadians, maxSpeed); - - // Used for the position hold feature - lastAngleScalar = angle; - - return speeds; - } - - /** - * Get the {@link ChassisSpeeds} based of raw speeds desired in meters/second and heading in - * radians. - * - * @param xSpeed X speed in meters per second. - * @param ySpeed Y speed in meters per second. - * @param omega Angular velocity in radians/second. - * @return {@link ChassisSpeeds} the robot should move to. - */ - public ChassisSpeeds getRawTargetSpeeds(double xSpeed, double ySpeed, double omega) { - if (xLimiter != null) { - xSpeed = xLimiter.calculate(xSpeed); - } - if (yLimiter != null) { - ySpeed = yLimiter.calculate(ySpeed); - } - if (angleLimiter != null) { - omega = angleLimiter.calculate(omega); - } - - return new ChassisSpeeds(xSpeed, ySpeed, omega); - } - - /** - * Get the {@link ChassisSpeeds} based of raw speeds desired in meters/second and heading in - * radians. - * - * @param xSpeed X speed in meters per second. - * @param ySpeed Y speed in meters per second. - * @param targetHeadingAngleRadians Target heading in radians. - * @param currentHeadingAngleRadians Current heading in radians. - * @return {@link ChassisSpeeds} the robot should move to. - */ - public ChassisSpeeds getRawTargetSpeeds( - double xSpeed, - double ySpeed, - double targetHeadingAngleRadians, - double currentHeadingAngleRadians) { - // Calculates an angular rate using a PIDController and the commanded angle. Returns a value - // between -1 and 1 - // which is then scaled to be between -maxAngularVelocity and +maxAngularVelocity. - return getRawTargetSpeeds( - xSpeed, - ySpeed, - thetaController.calculate(currentHeadingAngleRadians, targetHeadingAngleRadians) - * config.maxAngularVelocity); - } - - /** - * Calculate the angular velocity given the current and target heading angle in radians. - * - * @param currentHeadingAngleRadians The current heading of the robot in radians. - * @param targetHeadingAngleRadians The target heading of the robot in radians. - * @return Angular velocity in radians per second. - */ - public double headingCalculate( - double currentHeadingAngleRadians, double targetHeadingAngleRadians) { - return thetaController.calculate(currentHeadingAngleRadians, targetHeadingAngleRadians) - * config.maxAngularVelocity; - } - - /** - * Set a new maximum angular velocity that is different from the auto-generated one. Modified the - * {@link SwerveControllerConfiguration#maxAngularVelocity} field which is used in the {@link - * SwerveController} class for {@link ChassisSpeeds} generation. - * - * @param angularVelocity Angular velocity in radians per second. - */ - public void setMaximumChassisAngularVelocity(double angularVelocity) { - config.maxAngularVelocity = angularVelocity; - } -} diff --git a/src/main/java/swervelib/SwerveDrive.java b/src/main/java/swervelib/SwerveDrive.java deleted file mode 100644 index 6354afe1..00000000 --- a/src/main/java/swervelib/SwerveDrive.java +++ /dev/null @@ -1,1456 +0,0 @@ -package swervelib; - -import static edu.wpi.first.hal.FRCNetComm.tInstances.kRobotDriveSwerve_YAGSL; -import static edu.wpi.first.hal.FRCNetComm.tResourceType.kResourceType_RobotDrive; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.KilogramSquareMeters; -import static edu.wpi.first.units.Units.Kilograms; -import static edu.wpi.first.units.Units.Meters; -import static edu.wpi.first.units.Units.Newtons; -import static edu.wpi.first.units.Units.RadiansPerSecond; -import static edu.wpi.first.units.Units.Seconds; -import static edu.wpi.first.units.Units.Volts; - -import edu.wpi.first.hal.HAL; -import edu.wpi.first.math.Matrix; -import edu.wpi.first.math.controller.SimpleMotorFeedforward; -import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; -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.Rotation3d; -import edu.wpi.first.math.geometry.Transform2d; -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.kinematics.SwerveDriveKinematics; -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.math.numbers.N1; -import edu.wpi.first.math.numbers.N3; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.math.trajectory.Trajectory; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.networktables.DoublePublisher; -import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.units.measure.Force; -import edu.wpi.first.units.measure.LinearVelocity; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.Notifier; -import edu.wpi.first.wpilibj.TimedRobot; -import edu.wpi.first.wpilibj.Timer; -import edu.wpi.first.wpilibj.smartdashboard.Field2d; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import org.ironmaple.simulation.SimulatedArena; -import org.ironmaple.simulation.drivesims.AbstractDriveTrainSimulation; -import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; -import org.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig; -import org.ironmaple.simulation.drivesims.configs.SwerveModuleSimulationConfig; -import swervelib.encoders.CANCoderSwerve; -import swervelib.imu.Pigeon2Swerve; -import swervelib.imu.SwerveIMU; -import swervelib.math.SwerveMath; -import swervelib.motors.TalonFXSwerve; -import swervelib.parser.Cache; -import swervelib.parser.SwerveControllerConfiguration; -import swervelib.parser.SwerveDriveConfiguration; -import swervelib.simulation.SwerveIMUSimulation; -import swervelib.telemetry.SwerveDriveTelemetry; -import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; - -/** Swerve Drive class representing and controlling the swerve drive. */ -public class SwerveDrive implements AutoCloseable { - - /** Swerve Kinematics object. */ - public final SwerveDriveKinematics kinematics; - /** Swerve drive configuration. */ - public final SwerveDriveConfiguration swerveDriveConfiguration; - /** Swerve odometry. */ - public final SwerveDrivePoseEstimator swerveDrivePoseEstimator; - /** IMU reading cache for robot readings. */ - public final Cache imuReadingCache; - /** Swerve modules. */ - private final SwerveModule[] swerveModules; - /** WPILib {@link Notifier} to keep odometry up to date. */ - private final Notifier odometryThread; - /** Odometry lock to ensure thread safety. */ - private final Lock odometryLock = new ReentrantLock(); - /** Alert to recommend Tuner X if the configuration is compatible. */ - private final Alert tunerXRecommendation = - new Alert( - "Swerve Drive", - "Your Swerve Drive is compatible with Tuner X swerve generator, please consider using that instead of YAGSL. More information here!\n" - + "https://pro.docs.ctr-electronics.com/en/latest/docs/tuner/tuner-swerve/index.html", - AlertType.kWarning); - /** NT4 Publisher for the IMU reading. */ - private final DoublePublisher rawIMUPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/imu/raw") - .publish(); - /** NT4 Publisher for the IMU reading adjusted by offset and inversion. */ - private final DoublePublisher adjustedIMUPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/imu/adjusted") - .publish(); - /** Field object. */ - public Field2d field = new Field2d(); - /** Swerve controller for controlling heading of the robot. */ - public SwerveController swerveController; - /** - * Correct chassis velocity in {@link SwerveDrive#drive(Translation2d, double, boolean, boolean)} - * using 254's correction. - */ - public boolean chassisVelocityCorrection = true; - /** - * Correct chassis velocity in {@link SwerveDrive#setChassisSpeeds(ChassisSpeeds chassisSpeeds)} - * (auto) using 254's correction during auto. - */ - public boolean autonomousChassisVelocityCorrection = false; - /** - * Correct for skew that scales with angular velocity in {@link SwerveDrive#drive(Translation2d, - * double, boolean, boolean)} - */ - public boolean angularVelocityCorrection = false; - /** - * Correct for skew that scales with angular velocity in {@link - * SwerveDrive#setChassisSpeeds(ChassisSpeeds chassisSpeeds)} during auto. - */ - public boolean autonomousAngularVelocityCorrection = false; - /** Angular Velocity Correction Coefficent (expected values between -0.15 and 0.15). */ - public double angularVelocityCoefficient = 0; - /** Whether to correct heading when driving translationally. Set to true to enable. */ - public boolean headingCorrection = false; - /** MapleSim SwerveDrive. */ - private SwerveDriveSimulation mapleSimDrive; - /** Amount of seconds the duration of the timestep the speeds should be applied for. */ - private double discretizationdtSeconds = 0.02; - /** Deadband for speeds in heading correction. */ - private double HEADING_CORRECTION_DEADBAND = 0.01; - /** Swerve IMU device for sensing the heading of the robot. */ - private SwerveIMU imu; - /** Simulation of the swerve drive. */ - private SwerveIMUSimulation simIMU; - /** Counter to synchronize the modules relative encoder with absolute encoder when not moving. */ - private int moduleSynchronizationCounter = 0; - /** The last heading set in radians. */ - private double lastHeadingRadians = 0; - /** The absolute max speed that your robot can reach while translating in meters per second. */ - private double attainableMaxTranslationalSpeedMetersPerSecond = 0; - /** The absolute max speed the robot can reach while rotating radians per second. */ - private double attainableMaxRotationalVelocityRadiansPerSecond = 0; - /** Maximum speed of the robot in meters per second. */ - private double maxChassisSpeedMPS; - - /** - * Creates a new swerve drivebase subsystem. Robot is controlled via the {@link SwerveDrive#drive} - * method, or via the {@link SwerveDrive#setRawModuleStates} method. The {@link SwerveDrive#drive} - * method incorporates kinematics-- it takes a translation and rotation, as well as parameters for - * field-centric and closed-loop velocity control. {@link SwerveDrive#setRawModuleStates} takes a - * list of SwerveModuleStates and directly passes them to the modules. This subsystem also handles - * odometry. - * - * @param config The {@link SwerveDriveConfiguration} configuration to base the swerve drive off - * of. - * @param controllerConfig The {@link SwerveControllerConfiguration} to use when creating the - * {@link SwerveController}. - * @param maxSpeedMPS Maximum speed of the robot in meters per second, remember to use {@link - * Units#feetToMeters(double)} if you have feet per second! - * @param startingPose Starting {@link Pose2d} on the field. - */ - public SwerveDrive( - SwerveDriveConfiguration config, - SwerveControllerConfiguration controllerConfig, - double maxSpeedMPS, - Pose2d startingPose) { - this.attainableMaxTranslationalSpeedMetersPerSecond = this.maxChassisSpeedMPS = maxSpeedMPS; - this.attainableMaxRotationalVelocityRadiansPerSecond = - Math.PI * 2; // Defaulting to something reasonable for most robots - swerveDriveConfiguration = config; - swerveController = new SwerveController(controllerConfig); - // Create Kinematics from swerve module locations. - kinematics = new SwerveDriveKinematics(config.moduleLocationsMeters); - odometryThread = new Notifier(this::updateOdometry); - - this.swerveModules = config.modules; - - // Create an integrator for angle if the robot is being simulated to emulate an IMU - // If the robot is real, instantiate the IMU instead. - if (SwerveDriveTelemetry.isSimulation) { - DriveTrainSimulationConfig simulationConfig = - DriveTrainSimulationConfig.Default() - .withBumperSize( - Meters.of(config.getTracklength()).plus(Inches.of(5)), - Meters.of(config.getTrackwidth()).plus(Inches.of(5))) - .withRobotMass(Kilograms.of(config.physicalCharacteristics.robotMassKg)) - .withCustomModuleTranslations(config.moduleLocationsMeters) - .withGyro(config.getGyroSim()) - .withSwerveModule( - new SwerveModuleSimulationConfig( - config.getDriveMotorSim(), - config.getAngleMotorSim(), - config.physicalCharacteristics.conversionFactor.drive.gearRatio, - config.physicalCharacteristics.conversionFactor.angle.gearRatio, - Volts.of(config.physicalCharacteristics.driveFrictionVoltage), - Volts.of(config.physicalCharacteristics.angleFrictionVoltage), - Inches.of(config.physicalCharacteristics.conversionFactor.drive.diameter / 2), - KilogramSquareMeters.of(0.02), - config.physicalCharacteristics.wheelGripCoefficientOfFriction)); - - mapleSimDrive = new SwerveDriveSimulation(simulationConfig, startingPose); - - // feed module simulation instances to modules - for (int i = 0; i < swerveModules.length; i++) { - this.swerveModules[i].configureModuleSimulation( - mapleSimDrive.getModules()[i], config.physicalCharacteristics); - } - - // register the drivetrain simulation - SimulatedArena.getInstance().addDriveTrainSimulation(mapleSimDrive); - simIMU = new SwerveIMUSimulation(mapleSimDrive.getGyroSimulation()); - imuReadingCache = new Cache<>(simIMU::getGyroRotation3d, 5L); - } else { - imu = config.imu; - imu.factoryDefault(); - imuReadingCache = new Cache<>(imu::getRotation3d, 5L); - } - - // odometry = new SwerveDriveOdometry(kinematics, getYaw(), getModulePositions()); - swerveDrivePoseEstimator = - new SwerveDrivePoseEstimator( - kinematics, - getYaw(), - getModulePositions(), - startingPose); // x,y,heading in radians; Vision measurement std dev, higher=less weight - // - // Rotation3d currentGyro = imuReadingCache.getValue(); - // double offset = currentGyro.getZ() + - // startingPose.getRotation().getRadians(); - // setGyroOffset(new Rotation3d(currentGyro.getX(), currentGyro.getY(), offset)); - - // Initialize Telemetry - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.POSE.ordinal()) { - SmartDashboard.putData("Field", field); - } - - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { - SwerveDriveTelemetry.maxSpeed = maxSpeedMPS; - SwerveDriveTelemetry.maxAngularVelocity = swerveController.config.maxAngularVelocity; - SwerveDriveTelemetry.moduleCount = swerveModules.length; - SwerveDriveTelemetry.sizeFrontBack = - Units.metersToInches( - SwerveMath.getSwerveModule(swerveModules, true, false).moduleLocation.getX() - + SwerveMath.getSwerveModule(swerveModules, false, false).moduleLocation.getX()); - SwerveDriveTelemetry.sizeLeftRight = - Units.metersToInches( - SwerveMath.getSwerveModule(swerveModules, false, true).moduleLocation.getY() - + SwerveMath.getSwerveModule(swerveModules, false, false).moduleLocation.getY()); - SwerveDriveTelemetry.wheelLocations = new double[SwerveDriveTelemetry.moduleCount * 2]; - for (SwerveModule module : swerveModules) { - SwerveDriveTelemetry.wheelLocations[module.moduleNumber * 2] = - Units.metersToInches(module.configuration.moduleLocation.getX()); - SwerveDriveTelemetry.wheelLocations[(module.moduleNumber * 2) + 1] = - Units.metersToInches(module.configuration.moduleLocation.getY()); - } - SwerveDriveTelemetry.measuredStates = new double[SwerveDriveTelemetry.moduleCount * 2]; - SwerveDriveTelemetry.desiredStates = new double[SwerveDriveTelemetry.moduleCount * 2]; - SwerveDriveTelemetry.desiredStatesObj = - new SwerveModuleState[SwerveDriveTelemetry.moduleCount]; - SwerveDriveTelemetry.measuredStatesObj = - new SwerveModuleState[SwerveDriveTelemetry.moduleCount]; - } - - setOdometryPeriod(SwerveDriveTelemetry.isSimulation ? 0.004 : 0.02); - - checkIfTunerXCompatible(); - - HAL.report(kResourceType_RobotDrive, kRobotDriveSwerve_YAGSL); - } - - @Override - public void close() { - imu.close(); - tunerXRecommendation.close(); - - for (var module : swerveModules) { - module.close(); - } - } - - /** - * Update the cache validity period for the robot. - * - * @param imu IMU reading cache validity period in milliseconds. - * @param driveMotor Drive motor reading cache in milliseconds. - * @param absoluteEncoder Absolute encoder reading cache in milliseconds. - */ - public void updateCacheValidityPeriods(long imu, long driveMotor, long absoluteEncoder) { - imuReadingCache.updateValidityPeriod(imu); - for (SwerveModule module : swerveModules) { - module.drivePositionCache.updateValidityPeriod(driveMotor); - module.driveVelocityCache.updateValidityPeriod(driveMotor); - module.absolutePositionCache.updateValidityPeriod(absoluteEncoder); - } - } - - /** Check all components to ensure that Tuner X Swerve Generator is recommended instead. */ - private void checkIfTunerXCompatible() { - boolean compatible = imu instanceof Pigeon2Swerve; - for (SwerveModule module : swerveModules) { - compatible = - compatible - && module.getDriveMotor() instanceof TalonFXSwerve - && module.getAngleMotor() instanceof TalonFXSwerve - && module.getAbsoluteEncoder() instanceof CANCoderSwerve; - if (!compatible) { - break; - } - } - if (compatible) { - tunerXRecommendation.set(true); - } - } - - /** - * Set the odometry update period in seconds. - * - * @param period period in seconds. - */ - public void setOdometryPeriod(double period) { - odometryThread.stop(); - if (SwerveDriveTelemetry.isSimulation) { - SimulatedArena.overrideSimulationTimings(Seconds.of(period), 1); - } - odometryThread.startPeriodic(period); - } - - /** Stop the odometry thread in favor of manually updating odometry. */ - public void stopOdometryThread() { - odometryThread.stop(); - if (SwerveDriveTelemetry.isSimulation) { - SimulatedArena.overrideSimulationTimings(Seconds.of(TimedRobot.kDefaultPeriod), 5); - } - } - - /** - * Set the conversion factor for the angle/azimuth motor controller. - * - * @param conversionFactor Angle motor conversion factor for PID, should be generated from {@link - * SwerveMath#calculateDegreesPerSteeringRotation(double, double)} or calculated. - */ - public void setAngleMotorConversionFactor(double conversionFactor) { - for (SwerveModule module : swerveModules) { - module.setAngleMotorConversionFactor(conversionFactor); - } - } - - /** - * Set the conversion factor for the drive motor controller. - * - * @param conversionFactor Drive motor conversion factor for PID, should be generated from {@link - * SwerveMath#calculateMetersPerRotation(double, double, double)} or calculated. - */ - public void setDriveMotorConversionFactor(double conversionFactor) { - for (SwerveModule module : swerveModules) { - module.setDriveMotorConversionFactor(conversionFactor); - } - } - - /** - * Fetch the latest odometry heading, should be trusted over {@link SwerveDrive#getYaw()}. - * - * @return {@link Rotation2d} of the robot heading. - */ - public Rotation2d getOdometryHeading() { - return swerveDrivePoseEstimator.getEstimatedPosition().getRotation(); - } - - /** - * Set the heading correction capabilities of YAGSL. - * - * @param state {@link SwerveDrive#headingCorrection} state. - */ - public void setHeadingCorrection(boolean state) { - setHeadingCorrection(state, HEADING_CORRECTION_DEADBAND); - } - - /** - * Set the heading correction capabilities of YAGSL. - * - * @param state {@link SwerveDrive#headingCorrection} state. - * @param deadband {@link SwerveDrive#HEADING_CORRECTION_DEADBAND} deadband. - */ - public void setHeadingCorrection(boolean state, double deadband) { - headingCorrection = state; - HEADING_CORRECTION_DEADBAND = deadband; - } - - /** - * Tertiary method of controlling the drive base given velocity in both field oriented and robot - * oriented at the same time. The inputs are added together so this is not intended to be used to - * give the driver both methods of control. - * - * @param fieldOrientedVelocity The field oriented velocties to use - * @param robotOrientedVelocity The robot oriented velocties to use - */ - public void driveFieldOrientedAndRobotOriented( - ChassisSpeeds fieldOrientedVelocity, ChassisSpeeds robotOrientedVelocity) { - - drive( - ChassisSpeeds.fromFieldRelativeSpeeds(fieldOrientedVelocity, getOdometryHeading()) - .plus(robotOrientedVelocity)); - } - - /** - * Secondary method of controlling the drive base given velocity and adjusting it for field - * oriented use. - * - * @param fieldRelativeSpeeds Velocity of the robot desired. - */ - public void driveFieldOriented(ChassisSpeeds fieldRelativeSpeeds) { - drive(ChassisSpeeds.fromFieldRelativeSpeeds(fieldRelativeSpeeds, getOdometryHeading())); - } - - /** - * Secondary method of controlling the drive base given velocity and adjusting it for field - * oriented use. - * - * @param fieldRelativeSpeeds Velocity of the robot desired. - * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. - */ - public void driveFieldOriented( - ChassisSpeeds fieldRelativeSpeeds, Translation2d centerOfRotationMeters) { - drive( - ChassisSpeeds.fromFieldRelativeSpeeds(fieldRelativeSpeeds, getOdometryHeading()), - centerOfRotationMeters); - } - - /** - * Secondary method for controlling the drivebase. Given a simple {@link ChassisSpeeds} set the - * swerve module states, to achieve the goal. - * - * @param velocity The desired robot-oriented {@link ChassisSpeeds} for the robot to achieve. - */ - public void drive(ChassisSpeeds velocity) { - drive(velocity, false, new Translation2d()); - } - - /** - * Secondary method for controlling the drivebase. Given a simple {@link ChassisSpeeds} set the - * swerve module states, to achieve the goal. - * - * @param velocity The desired robot-oriented {@link ChassisSpeeds} for the robot to achieve. - * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. - */ - public void drive(ChassisSpeeds velocity, Translation2d centerOfRotationMeters) { - drive(velocity, false, centerOfRotationMeters); - } - - /** - * The primary method for controlling the drivebase. Takes a {@link Translation2d} and a rotation - * rate, and calculates and commands module states accordingly. Can use either open-loop or - * closed-loop velocity control for the wheel velocities. Also has field- and robot-relative - * modes, which affect how the translation vector is used. - * - * @param translation {@link Translation2d} that is the commanded linear velocity of the robot, in - * meters per second. In robot-relative mode, positive x is torwards the bow (front) and - * positive y is torwards port (left). In field-relative mode, positive x is away from the - * alliance wall (field North) and positive y is torwards the left wall when looking through - * the driver station glass (field West). - * @param rotation Robot angular rate, in radians per second. CCW positive. Unaffected by - * field/robot relativity. - * @param fieldRelative Drive mode. True for field-relative, false for robot-relative. - * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable - * closed-loop. - * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. - */ - public void drive( - Translation2d translation, - double rotation, - boolean fieldRelative, - boolean isOpenLoop, - Translation2d centerOfRotationMeters) { - // Creates a robot-relative ChassisSpeeds object, converting from field-relative speeds if - // necessary. - ChassisSpeeds velocity = new ChassisSpeeds(translation.getX(), translation.getY(), rotation); - if (fieldRelative) { - velocity = ChassisSpeeds.fromFieldRelativeSpeeds(velocity, getOdometryHeading()); - } - drive(velocity, isOpenLoop, centerOfRotationMeters); - } - - /** - * The primary method for controlling the drivebase. Takes a {@link Translation2d} and a rotation - * rate, and calculates and commands module states accordingly. Can use either open-loop or - * closed-loop velocity control for the wheel velocities. Also has field- and robot-relative - * modes, which affect how the translation vector is used. - * - * @param translation {@link Translation2d} that is the commanded linear velocity of the robot, in - * meters per second. In robot-relative mode, positive x is torwards the bow (front) and - * positive y is torwards port (left). In field-relative mode, positive x is away from the - * alliance wall (field North) and positive y is torwards the left wall when looking through - * the driver station glass (field West). - * @param rotation Robot angular rate, in radians per second. CCW positive. Unaffected by - * field/robot relativity. - * @param fieldRelative Drive mode. True for field-relative, false for robot-relative. - * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable - * closed-loop. - */ - public void drive( - Translation2d translation, double rotation, boolean fieldRelative, boolean isOpenLoop) { - // Creates a robot-relative ChassisSpeeds object, converting from field-relative speeds if - // necessary. - ChassisSpeeds velocity = new ChassisSpeeds(translation.getX(), translation.getY(), rotation); - - if (fieldRelative) { - velocity = ChassisSpeeds.fromFieldRelativeSpeeds(velocity, getOdometryHeading()); - } - drive(velocity, isOpenLoop, new Translation2d()); - } - - /** - * The primary method for controlling the drivebase. Takes a {@link ChassisSpeeds}, and calculates - * and commands module states accordingly. Can use either open-loop or closed-loop velocity - * control for the wheel velocities. Applies heading correction if enabled and necessary. - * - * @param robotRelativeVelocity The chassis speeds to set the robot to achieve. - * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable - * closed-loop. - * @param centerOfRotationMeters The center of rotation in meters, 0 is the center of the robot. - */ - public void drive( - ChassisSpeeds robotRelativeVelocity, - boolean isOpenLoop, - Translation2d centerOfRotationMeters) { - SwerveDriveTelemetry.startCtrlCycle(); - robotRelativeVelocity = - movementOptimizations( - robotRelativeVelocity, chassisVelocityCorrection, angularVelocityCorrection); - - // Heading Angular Velocity Deadband, might make a configuration option later. - // Originally made by Team 1466 Webb Robotics. - // Modified by Team 7525 Pioneers and BoiledBurntBagel of 6036 - if (headingCorrection) { - if (Math.abs(robotRelativeVelocity.omegaRadiansPerSecond) < HEADING_CORRECTION_DEADBAND - && (Math.abs(robotRelativeVelocity.vxMetersPerSecond) > HEADING_CORRECTION_DEADBAND - || Math.abs(robotRelativeVelocity.vyMetersPerSecond) > HEADING_CORRECTION_DEADBAND)) { - robotRelativeVelocity.omegaRadiansPerSecond = - swerveController.headingCalculate( - getOdometryHeading().getRadians(), lastHeadingRadians); - } else { - lastHeadingRadians = getOdometryHeading().getRadians(); - } - } - - // Display commanded speed for testing - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.LOW.ordinal()) { - SwerveDriveTelemetry.desiredChassisSpeedsObj = robotRelativeVelocity; - } - - // Calculate required module states via kinematics - SwerveModuleState[] swerveModuleStates = - kinematics.toSwerveModuleStates(robotRelativeVelocity, centerOfRotationMeters); - - setRawModuleStates(swerveModuleStates, robotRelativeVelocity, isOpenLoop); - } - - /** - * Set the maximum attainable speeds for desaturation. - * - * @param attainableMaxTranslationalSpeedMetersPerSecond The absolute max speed that your robot - * can reach while translating in meters per second. - * @param attainableMaxRotationalVelocityRadiansPerSecond The absolute max speed the robot can - * reach while rotating in radians per second. - */ - public void setMaximumAttainableSpeeds( - double attainableMaxTranslationalSpeedMetersPerSecond, - double attainableMaxRotationalVelocityRadiansPerSecond) { - this.attainableMaxTranslationalSpeedMetersPerSecond = - attainableMaxTranslationalSpeedMetersPerSecond; - this.attainableMaxRotationalVelocityRadiansPerSecond = - attainableMaxRotationalVelocityRadiansPerSecond; - } - - /** - * Set the maximum allowable speeds for desaturation. - * - * @param maxTranslationalSpeedMetersPerSecond The allowable max speed that your robot should - * reach while translating in meters per second. - * @param maxRotationalVelocityRadiansPerSecond The allowable max speed the robot should reach - * while rotating in radians per second. - */ - public void setMaximumAllowableSpeeds( - double maxTranslationalSpeedMetersPerSecond, double maxRotationalVelocityRadiansPerSecond) { - this.maxChassisSpeedMPS = maxTranslationalSpeedMetersPerSecond; - this.swerveController.config.maxAngularVelocity = maxRotationalVelocityRadiansPerSecond; - } - - /** - * Get the maximum velocity from {@link - * SwerveDrive#attainableMaxTranslationalSpeedMetersPerSecond} or {@link - * SwerveDrive#maxChassisSpeedMPS} whichever is the lower limit on the robot's speed. - * - * @return Minimum speed in meters/second of physically attainable and user allowable limits. - */ - public double getMaximumChassisVelocity() { - return Math.min(this.attainableMaxTranslationalSpeedMetersPerSecond, maxChassisSpeedMPS); - } - - /** - * Get the maximum drive velocity of a module as a {@link LinearVelocity}. - * - * @return {@link LinearVelocity} representing the maximum drive speed of a module. - */ - public double getMaximumModuleDriveVelocity() { - return swerveModules[0].getMaxDriveVelocityMetersPerSecond(); - } - - /** - * Get the maximum angular velocity of an azimuth/angle motor in the swerve module. - * - * @return {@link AngularVelocity} of the maximum azimuth/angle motor. - */ - public AngularVelocity getMaximumModuleAngleVelocity() { - return swerveModules[0].getMaxAngularVelocity(); - } - - /** - * Get the maximum angular velocity, either {@link - * SwerveDrive#attainableMaxRotationalVelocityRadiansPerSecond} or {@link - * SwerveControllerConfiguration#maxAngularVelocity}, whichever is the lower limit on the robot's - * speed. - * - * @return Minimum angular velocity in radians per second of physically attainable and user - * allowable limits. - */ - public double getMaximumChassisAngularVelocity() { - return Math.min( - this.attainableMaxRotationalVelocityRadiansPerSecond, - swerveController.config.maxAngularVelocity); - } - - /** - * Set the module states (azimuth and velocity) directly. - * - * @param desiredStates A list of SwerveModuleStates to send to the modules. - * @param desiredChassisSpeed The desired chassis speeds to set the robot to achieve. - * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable - * closed-loop. - */ - private void setRawModuleStates( - SwerveModuleState[] desiredStates, ChassisSpeeds desiredChassisSpeed, boolean isOpenLoop) { - // Desaturates wheel speeds - double maxModuleSpeedMPS = getMaximumModuleDriveVelocity(); - if ((attainableMaxTranslationalSpeedMetersPerSecond != 0 - || attainableMaxRotationalVelocityRadiansPerSecond != 0) - && attainableMaxTranslationalSpeedMetersPerSecond != maxChassisSpeedMPS) { - SwerveDriveKinematics.desaturateWheelSpeeds( - desiredStates, - desiredChassisSpeed, - maxModuleSpeedMPS, - attainableMaxTranslationalSpeedMetersPerSecond, - attainableMaxRotationalVelocityRadiansPerSecond); - } else { - SwerveDriveKinematics.desaturateWheelSpeeds(desiredStates, maxModuleSpeedMPS); - } - - // Sets states - for (SwerveModule module : swerveModules) { - module.setDesiredState(desiredStates[module.moduleNumber], isOpenLoop, false); - } - } - - /** - * Set the module states (azimuth and velocity) directly. Used primarily for auto paths. Does not - * allow for usage of {@link SwerveDriveKinematics#desaturateWheelSpeeds(SwerveModuleState[] - * moduleStates, ChassisSpeeds desiredChassisSpeed, double - * attainableMaxModuleSpeedMetersPerSecond, double attainableMaxTranslationalSpeedMetersPerSecond, - * double attainableMaxRotationalVelocityRadiansPerSecond)} - * - * @param desiredStates A list of SwerveModuleStates to send to the modules. - * @param isOpenLoop Whether to use closed-loop velocity control. Set to true to disable - * closed-loop. - */ - public void setModuleStates(SwerveModuleState[] desiredStates, boolean isOpenLoop) { - SwerveDriveTelemetry.startCtrlCycle(); - double maxModuleSpeedMPS = getMaximumModuleDriveVelocity(); - desiredStates = kinematics.toSwerveModuleStates(kinematics.toChassisSpeeds(desiredStates)); - SwerveDriveKinematics.desaturateWheelSpeeds(desiredStates, maxModuleSpeedMPS); - - // Sets states - for (SwerveModule module : swerveModules) { - module.setDesiredState(desiredStates[module.moduleNumber], isOpenLoop, false); - } - } - - /** - * Drive the robot using the {@link SwerveModuleState}, it is recommended to have {@link - * SwerveDrive#setCosineCompensator(boolean)} set to false for this.
- * - * @param robotRelativeVelocity Robot relative {@link ChassisSpeeds} - * @param states Corresponding {@link SwerveModuleState} to use (not checked against the {@param - * robotRelativeVelocity}). - * @param feedforwardForces Feedforward forces generated by set-point generator - */ - public void drive( - ChassisSpeeds robotRelativeVelocity, SwerveModuleState[] states, Force[] feedforwardForces) { - SwerveDriveTelemetry.startCtrlCycle(); - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.LOW.ordinal()) { - SwerveDriveTelemetry.desiredChassisSpeedsObj = robotRelativeVelocity; - } - for (SwerveModule module : swerveModules) { - module.applyStateOptimizations(states[module.moduleNumber]); - module.applyAntiJitter(states[module.moduleNumber], false); - - // from the module configuration, obtain necessary information to calculate feed-forward - // Warning: Will not work well if motor is not what we are expecting. - // Warning: Should replace module.getDriveMotor().simMotor with expected motor type first. - DCMotor driveMotorModel = module.configuration.driveMotor.getSimMotor(); - double driveGearRatio = module.configuration.conversionFactors.drive.gearRatio; - double wheelRadiusMeters = - Units.inchesToMeters(module.configuration.conversionFactors.drive.diameter) / 2; - - // calculation: - double desiredGroundSpeedMPS = states[module.moduleNumber].speedMetersPerSecond; - double feedforwardVoltage = - driveMotorModel.getVoltage( - // Since: (1) torque = force * momentOfForce; (2) torque (on wheel) = torque (on - // motor) * gearRatio - // torque (on motor) = force * wheelRadius / gearRatio - feedforwardForces[module.moduleNumber].in(Newtons) - * wheelRadiusMeters - / driveGearRatio, - // Since: (1) linear velocity = angularVelocity * wheelRadius; (2) wheelVelocity = - // motorVelocity / gearRatio - // motorAngularVelocity = linearVelocity / wheelRadius * gearRatio - desiredGroundSpeedMPS / wheelRadiusMeters * driveGearRatio); - module.setDesiredState(states[module.moduleNumber], false, feedforwardVoltage); - } - } - - /** - * Set chassis speeds with closed-loop velocity control. - * - * @param robotRelativeSpeeds Chassis speeds to set. - */ - public void setChassisSpeeds(ChassisSpeeds robotRelativeSpeeds) { - SwerveDriveTelemetry.startCtrlCycle(); - robotRelativeSpeeds = - movementOptimizations( - robotRelativeSpeeds, - autonomousChassisVelocityCorrection, - autonomousAngularVelocityCorrection); - - SwerveDriveTelemetry.desiredChassisSpeedsObj = robotRelativeSpeeds; - - setRawModuleStates( - kinematics.toSwerveModuleStates(robotRelativeSpeeds), robotRelativeSpeeds, false); - } - - /** - * Gets the measured pose (position and rotation) of the robot, as reported by odometry. - * - * @return The robot's pose - */ - public Pose2d getPose() { - - odometryLock.lock(); - Pose2d poseEstimation = swerveDrivePoseEstimator.getEstimatedPosition(); - odometryLock.unlock(); - return poseEstimation; - } - - /** - * Gets the maple-sim drivetrain simulation instance This is used to add intake simulation / - * launch game pieces from the robot - * - * @return an optional maple-sim {@link SwerveDriveSimulation} object, or {@link Optional#empty()} - * when calling from a real robot - */ - public Optional getMapleSimDrive() { - if (SwerveDriveTelemetry.isSimulation) { - return Optional.of(mapleSimDrive); - } - - return Optional.empty(); - } - - /** - * Gets the actual pose of the drivetrain during simulation - * - * @return an {@link Optional} {@link Pose2d}, representing the drivetrain pose during simulation, - * or an empty optional when running on real robot - */ - public Optional getSimulationDriveTrainPose() { - odometryLock.lock(); - Optional simulationPose = - getMapleSimDrive().map(AbstractDriveTrainSimulation::getSimulatedDriveTrainPose); - odometryLock.unlock(); - return simulationPose; - } - - /** - * Gets the measured field-relative robot velocity (x, y and omega) - * - * @return A ChassisSpeeds object of the current field-relative velocity - */ - public ChassisSpeeds getFieldVelocity() { - // ChassisSpeeds has a method to convert from field-relative to robot-relative speeds, - // but not the reverse. However, because this transform is a simple rotation, negating the - // angle given as the robot angle reverses the direction of rotation, and the conversion is - // reversed. - ChassisSpeeds robotRelativeSpeeds = kinematics.toChassisSpeeds(getStates()); - return ChassisSpeeds.fromRobotRelativeSpeeds(robotRelativeSpeeds, getOdometryHeading()); - // Might need to be this instead - // return ChassisSpeeds.fromFieldRelativeSpeeds( - // kinematics.toChassisSpeeds(getStates()), getOdometryHeading().unaryMinus()); - } - - /** - * Gets the current robot-relative velocity (x, y and omega) of the robot - * - * @return A ChassisSpeeds object of the current robot-relative velocity - */ - public ChassisSpeeds getRobotVelocity() { - return kinematics.toChassisSpeeds(getStates()); - } - - /** - * Resets odometry to the given pose. Gyro angle and module positions do not need to be reset when - * calling this method. However, if either gyro angle or module position is reset, this must be - * called in order for odometry to keep working. - * - * @param pose The pose to set the odometry to. Field relative, blue-origin where 0deg is facing - * towards RED alliance. - */ - public void resetOdometry(Pose2d pose) { - odometryLock.lock(); - swerveDrivePoseEstimator.resetPosition(getYaw(), getModulePositions(), pose); - if (SwerveDriveTelemetry.isSimulation) { - mapleSimDrive.setSimulationWorldPose(pose); - } - odometryLock.unlock(); - ChassisSpeeds robotRelativeSpeeds = - ChassisSpeeds.fromRobotRelativeSpeeds(new ChassisSpeeds(0, 0, 0), getYaw()); - kinematics.toSwerveModuleStates(robotRelativeSpeeds); - } - - /** - * Post the trajectory to the field - * - * @param trajectory the trajectory to post. - */ - public void postTrajectory(Trajectory trajectory) { - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.POSE.ordinal()) { - field.getObject("Trajectory").setTrajectory(trajectory); - } - } - - /** - * Gets the current module states (azimuth and velocity) - * - * @return A list of SwerveModuleStates containing the current module states - */ - public SwerveModuleState[] getStates() { - SwerveModuleState[] states = new SwerveModuleState[swerveDriveConfiguration.moduleCount]; - for (SwerveModule module : swerveModules) { - states[module.moduleNumber] = module.getState(); - } - return states; - } - - /** - * Gets the current module positions (azimuth and wheel position (meters)). - * - * @return A list of SwerveModulePositions containg the current module positions - */ - public SwerveModulePosition[] getModulePositions() { - SwerveModulePosition[] positions = - new SwerveModulePosition[swerveDriveConfiguration.moduleCount]; - for (SwerveModule module : swerveModules) { - positions[module.moduleNumber] = module.getPosition(); - } - return positions; - } - - /** - * Getter for the {@link SwerveIMU}. - * - * @return generated {@link SwerveIMU} - */ - public SwerveIMU getGyro() { - return swerveDriveConfiguration.imu; - } - - /** - * Set the expected gyroscope angle using a {@link Rotation3d} object. To reset gyro, set to a new - * {@link Rotation3d} subtracted from the current gyroscopic readings {@link - * SwerveIMU#getRotation3d()}. - * - * @param gyro expected gyroscope angle as {@link Rotation3d}. - */ - public void setGyro(Rotation3d gyro) { - if (SwerveDriveTelemetry.isSimulation) { - setGyroOffset(simIMU.getGyroRotation3d().minus(gyro)); - } else { - setGyroOffset(imu.getRawRotation3d().minus(gyro)); - } - imuReadingCache.update(); - } - - /** - * Resets the gyro angle to zero and resets odometry to the same position, but facing toward 0 - * (red alliance station). - */ - public void zeroGyro() { - // Resets the real gyro or the angle accumulator, depending on whether the robot is being - // simulated - if (SwerveDriveTelemetry.isSimulation) { - simIMU.setAngle(0); - } else { - setGyroOffset(imu.getRawRotation3d()); - } - imuReadingCache.update(); - swerveController.lastAngleScalar = 0; - lastHeadingRadians = 0; - resetOdometry(new Pose2d(getPose().getTranslation(), new Rotation2d())); - } - - /** - * Gets the current yaw angle of the robot, as reported by the imu. CCW positive, not wrapped. - * - * @return The yaw as a {@link Rotation2d} angle - */ - public Rotation2d getYaw() { - // Read the imu if the robot is real or the accumulator if the robot is simulated. - return Rotation2d.fromRadians(imuReadingCache.getValue().getZ()); - } - - /** - * Gets the current pitch angle of the robot, as reported by the imu. - * - * @return The heading as a {@link Rotation2d} angle - */ - public Rotation2d getPitch() { - // Read the imu if the robot is real or the accumulator if the robot is simulated. - return Rotation2d.fromRadians(imuReadingCache.getValue().getY()); - } - - /** - * Gets the current roll angle of the robot, as reported by the imu. - * - * @return The heading as a {@link Rotation2d} angle - */ - public Rotation2d getRoll() { - // Read the imu if the robot is real or the accumulator if the robot is simulated. - return Rotation2d.fromRadians(imuReadingCache.getValue().getX()); - } - - /** - * Gets the current gyro {@link Rotation3d} of the robot, as reported by the imu. - * - * @return The heading as a {@link Rotation3d} angle - */ - public Rotation3d getGyroRotation3d() { - // Read the imu if the robot is real or the accumulator if the robot is simulated. - return imuReadingCache.getValue(); - } - - /** - * Gets current acceleration of the robot in m/s/s. If gyro unsupported returns empty. - * - * @return acceleration of the robot as a {@link Translation3d} - */ - public Optional getAccel() { - if (!SwerveDriveTelemetry.isSimulation) { - return imu.getAccel(); - } else { - return simIMU.getAccel(); - } - } - - /** - * Sets the drive motors to brake/coast mode. - * - * @param brake True to set motors to brake mode, false for coast. - */ - public void setMotorIdleMode(boolean brake) { - for (SwerveModule swerveModule : swerveModules) { - swerveModule.setMotorBrake(brake); - } - } - - /** - * Enable auto synchronization for encoders during a match. This will only occur when the modules - * are not moving for a few seconds. - * - * @param enabled Enable state - * @param deadband Deadband in degrees, default is 3 degrees. - */ - public void setModuleEncoderAutoSynchronize(boolean enabled, double deadband) { - for (SwerveModule swerveModule : swerveModules) { - swerveModule.setEncoderAutoSynchronize(enabled, deadband); - } - } - - /** - * Point all modules toward the robot center, thus making the robot very difficult to move. - * Forcing the robot to keep the current pose. - */ - public void lockPose() { - // Sets states - for (SwerveModule swerveModule : swerveModules) { - SwerveModuleState desiredState = - new SwerveModuleState(0, swerveModule.configuration.moduleLocation.getAngle()); - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { - SwerveDriveTelemetry.desiredStatesObj[swerveModule.moduleNumber] = desiredState; - } - swerveModule.setDesiredState(desiredState, false, true); - } - - // Update kinematics because we are not using setModuleStates - kinematics.toSwerveModuleStates(new ChassisSpeeds()); - } - - /** - * Get the swerve module poses and on the field relative to the robot. - * - * @param robotPose Robot pose. - * @return Swerve module poses. - */ - public Pose2d[] getSwerveModulePoses(Pose2d robotPose) { - Pose2d[] poseArr = new Pose2d[swerveDriveConfiguration.moduleCount]; - List poses = new ArrayList<>(); - for (SwerveModule module : swerveModules) { - poses.add( - robotPose.plus( - new Transform2d(module.configuration.moduleLocation, module.getState().angle))); - } - return poses.toArray(poseArr); - } - - /** - * Setup the swerve module feedforward. - * - * @param driveFeedforward Feedforward for the drive motor on swerve modules. - */ - public void replaceSwerveModuleFeedforward(SimpleMotorFeedforward driveFeedforward) { - for (SwerveModule swerveModule : swerveModules) { - swerveModule.setFeedforward(driveFeedforward); - } - } - - /** - * Update odometry should be run every loop. Synchronizes module absolute encoders with relative - * encoders periodically. In simulation mode will also post the pose of each module. Updates - * SmartDashboard with module encoder readings and states. - */ - public void updateOdometry() { - SwerveDriveTelemetry.startOdomCycle(); - odometryLock.lock(); - // invalidateCache(); - try { - // Update odometry - swerveDrivePoseEstimator.update(getYaw(), getModulePositions()); - - if (SwerveDriveTelemetry.isSimulation) { - try { - SimulatedArena.getInstance().simulationPeriodic(); - } catch (Exception e) { - DriverStation.reportError("MapleSim error", false); - } - } - - // Update angle accumulator if the robot is simulated - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { - SwerveDriveTelemetry.measuredChassisSpeedsObj = getRobotVelocity(); - SwerveDriveTelemetry.robotRotationObj = getOdometryHeading(); - } - - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.POSE.ordinal()) { - if (SwerveDriveTelemetry.isSimulation) { - field.setRobotPose(mapleSimDrive.getSimulatedDriveTrainPose()); - field.getObject("OdometryPose").setPose(swerveDrivePoseEstimator.getEstimatedPosition()); - field - .getObject("XModules") - .setPoses(getSwerveModulePoses(mapleSimDrive.getSimulatedDriveTrainPose())); - - } else { - field.setRobotPose(swerveDrivePoseEstimator.getEstimatedPosition()); - } - } - - double sumVelocity = 0; - for (SwerveModule module : swerveModules) { - SwerveModuleState moduleState = module.getState(); - sumVelocity += Math.abs(moduleState.speedMetersPerSecond); - if (SwerveDriveTelemetry.verbosity == TelemetryVerbosity.HIGH) { - module.updateTelemetry(); - rawIMUPublisher.set(getYaw().getDegrees()); - adjustedIMUPublisher.set(getOdometryHeading().getDegrees()); - } - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { - SwerveDriveTelemetry.measuredStatesObj[module.moduleNumber] = moduleState; - } - } - - // If the robot isn't moving synchronize the encoders every 100ms (Inspired by democrat's SDS - // lib) - // To ensure that everytime we initialize it works. - if (sumVelocity <= .01 && ++moduleSynchronizationCounter > 5) { - synchronizeModuleEncoders(); - moduleSynchronizationCounter = 0; - } - - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { - SwerveDriveTelemetry.updateData(); - } - } catch (Exception e) { - odometryLock.unlock(); - throw e; - } - odometryLock.unlock(); - SwerveDriveTelemetry.endOdomCycle(); - } - - /** Invalidate all {@link Cache} object used by the {@link SwerveDrive} */ - public void invalidateCache() { - imuReadingCache.update(); - for (SwerveModule module : swerveModules) { - module.invalidateCache(); - } - } - - /** Synchronize angle motor integrated encoders with data from absolute encoders. */ - public void synchronizeModuleEncoders() { - for (SwerveModule module : swerveModules) { - module.queueSynchronizeEncoders(); - } - } - - /** - * Set the gyro scope offset to a desired known rotation. Unlike {@link - * SwerveDrive#setGyro(Rotation3d)} it DOES NOT take the current rotation into account. - * - * @param offset {@link Rotation3d} known offset of the robot for gyroscope to use. - */ - public void setGyroOffset(Rotation3d offset) { - if (SwerveDriveTelemetry.isSimulation) { - simIMU.setAngle(offset.getZ()); - } else { - imu.setOffset(offset); - } - imuReadingCache.update(); - } - - /** - * Add a vision measurement to the {@link SwerveDrivePoseEstimator} and update the {@link - * SwerveIMU} gyro reading with the given timestamp of the vision measurement. - * - * @param robotPose Robot {@link Pose2d} as measured by vision. - * @param timestamp Timestamp the measurement was taken as time since startup, should be taken - * from {@link Timer#getFPGATimestamp()} or similar sources. - * @param visionMeasurementStdDevs Vision measurement standard deviation that will be sent to the - * {@link SwerveDrivePoseEstimator}.The standard deviation of the vision measurement, for best - * accuracy calculate the standard deviation at 2 or more points and fit a line to it with the - * calculated optimal standard deviation. (Units should be meters per pixel). By optimizing - * this you can get * vision accurate to inches instead of feet. - */ - public void addVisionMeasurement( - Pose2d robotPose, double timestamp, Matrix visionMeasurementStdDevs) { - odometryLock.lock(); - swerveDrivePoseEstimator.addVisionMeasurement(robotPose, timestamp, visionMeasurementStdDevs); - odometryLock.unlock(); - } - - /** - * Sets the pose estimator's trust of global measurements. This might be used to change trust in - * vision measurements after the autonomous period, or to change trust as distance to a vision - * target increases. - * - * @param visionMeasurementStdDevs Standard deviations of the vision measurements. Increase these - * numbers to trust global measurements from vision less. This matrix is in the form [x, y, - * theta], with units in meters and radians. - */ - public void setVisionMeasurementStdDevs(Matrix visionMeasurementStdDevs) { - odometryLock.lock(); - swerveDrivePoseEstimator.setVisionMeasurementStdDevs(visionMeasurementStdDevs); - odometryLock.unlock(); - } - - /** - * Add a vision measurement to the {@link SwerveDrivePoseEstimator} and update the {@link - * SwerveIMU} gyro reading with the given timestamp of the vision measurement. - * - * @param robotPose Robot {@link Pose2d} as measured by vision. - * @param timestamp Timestamp the measurement was taken as time since startup, should be taken - * from {@link Timer#getFPGATimestamp()} or similar sources. - */ - public void addVisionMeasurement(Pose2d robotPose, double timestamp) { - odometryLock.lock(); - swerveDrivePoseEstimator.addVisionMeasurement(robotPose, timestamp); - // Pose2d newOdometry = new - // Pose2d(swerveDrivePoseEstimator.getEstimatedPosition().getTranslation(), - // robotPose.getRotation()); - odometryLock.unlock(); - - // setGyroOffset(new Rotation3d(0, 0, robotPose.getRotation().getRadians())); - // resetOdometry(newOdometry); - } - - /** - * Helper function to get the {@link SwerveDrive#swerveController} for the {@link SwerveDrive} - * which can be used to generate {@link ChassisSpeeds} for the robot to orient it correctly given - * axis or angles, and apply {@link edu.wpi.first.math.filter.SlewRateLimiter} to given inputs. - * Important functions to look at are {@link SwerveController#getTargetSpeeds(double, double, - * double, double, double)}, {@link SwerveController#addSlewRateLimiters(SlewRateLimiter, - * SlewRateLimiter, SlewRateLimiter)}, {@link SwerveController#getRawTargetSpeeds(double, double, - * double)}. - * - * @return {@link SwerveController} for the {@link SwerveDrive}. - */ - public SwerveController getSwerveController() { - return swerveController; - } - - /** - * Get the {@link SwerveModule}s associated with the {@link SwerveDrive}. - * - * @return {@link SwerveModule} array specified by configurations. - */ - public SwerveModule[] getModules() { - return swerveDriveConfiguration.modules; - } - - /** - * Get the {@link SwerveModule}'s as a {@link HashMap} where the key is the swerve module - * configuration name. - * - * @return {@link HashMap}(Module Name, SwerveModule) - */ - public Map getModuleMap() { - Map map = new HashMap(); - for (SwerveModule module : swerveModules) { - map.put(module.configuration.name, module); - } - return map; - } - - /** - * Reset the drive encoders on the robot, useful when manually resetting the robot without a - * reboot, like in autonomous. - */ - public void resetDriveEncoders() { - for (SwerveModule module : swerveModules) { - module.getDriveMotor().setPosition(0); - } - } - - /** - * Set the motor controller closed loop feedback device to the defined external absolute encoder, - * with the given offset from the supplied configuration, overwriting any native offset. - */ - public void useExternalFeedbackSensor() { - for (SwerveModule module : swerveModules) { - module.useExternalFeedbackSensor(); - } - } - - /** - * Set the motor controller closed loop feedback device to the internal encoder instead of the - * absolute encoder. - */ - public void useInternalFeedbackSensor() { - for (SwerveModule module : swerveModules) { - module.useInternalFeedbackSensor(); - } - } - - /** - * Pushes the Absolute Encoder offsets to the Encoder or Motor Controller, depending on type. Also - * removes the internal offsets to prevent double offsetting. - */ - @Deprecated - public void pushOffsetsToEncoders() { - for (SwerveModule module : swerveModules) { - module.pushOffsetsToEncoders(); - } - } - - /** Restores Internal YAGSL Encoder offsets and sets the Encoder stored offset back to 0 */ - @Deprecated - public void restoreInternalOffset() { - for (SwerveModule module : swerveModules) { - module.restoreInternalOffset(); - } - } - - /** - * Set module optimization to be utilized or not. Sometimes it is desirable to be enabled for - * debugging purposes only. - * - * @param enabled Optimization enabled state. - */ - public void setModuleStateOptimization(boolean enabled) { - for (SwerveModule module : swerveModules) { - module.setModuleStateOptimization(enabled); - } - } - - /** - * Enable auto-centering module wheels. This has a side effect of causing some jitter to the robot - * when a PID is not tuned perfectly. This function is a wrapper for {@link - * SwerveModule#setAntiJitter(boolean)} to perform auto-centering. - * - * @param enabled Enable auto-centering (disable antiJitter) - */ - public void setAutoCenteringModules(boolean enabled) { - for (SwerveModule module : swerveModules) { - module.setAntiJitter(!enabled); - } - } - - /** - * Enable or disable the {@link swervelib.parser.SwerveModuleConfiguration#useCosineCompensator} - * for all {@link SwerveModule}'s in the swerve drive. The cosine compensator will slow down or - * speed up modules that are close to their desired state in theory. - * - * @param enabled Usage of the cosine compensator. - */ - public void setCosineCompensator(boolean enabled) { - for (SwerveModule module : swerveModules) { - module.configuration.useCosineCompensator = enabled; - } - } - - /** - * Sets the Chassis discretization seconds as well as enableing/disabling the Chassis velocity - * correction in teleop - * - * @param enable Enable chassis velocity correction, which will use {@link - * ChassisSpeeds#discretize(ChassisSpeeds, double)}} with the following. - * @param dtSeconds The duration of the timestep the speeds should be applied for. - */ - public void setChassisDiscretization(boolean enable, double dtSeconds) { - if (!SwerveDriveTelemetry.isSimulation) { - chassisVelocityCorrection = enable; - discretizationdtSeconds = dtSeconds; - } - } - - /** - * Sets the Chassis discretization seconds as well as enableing/disabling the Chassis velocity - * correction in teleop and/or auto - * - * @param useInTeleop Enable chassis velocity correction, which will use {@link - * ChassisSpeeds#discretize(ChassisSpeeds, double)} with the following in teleop. - * @param useInAuto Enable chassis velocity correction, which will use {@link - * ChassisSpeeds#discretize(ChassisSpeeds, double)} with the following in auto. - * @param dtSeconds The duration of the timestep the speeds should be applied for. - */ - public void setChassisDiscretization(boolean useInTeleop, boolean useInAuto, double dtSeconds) { - if (!SwerveDriveTelemetry.isSimulation) { - chassisVelocityCorrection = useInTeleop; - autonomousChassisVelocityCorrection = useInAuto; - discretizationdtSeconds = dtSeconds; - } - } - - /** - * Enables angular velocity skew correction in teleop and/or autonomous and sets the angular - * velocity coefficient for both modes - * - * @param useInTeleop Enables angular velocity correction in teleop. - * @param useInAuto Enables angular velocity correction in autonomous. - * @param angularVelocityCoeff The angular velocity coefficient. Expected values between -0.15 to - * 0.15. Start with a value of 0.1, test in teleop. When enabling for the first time if the - * skew is significantly worse try inverting the value. Tune by moving in a straight line - * while rotating. Testing is best done with angular velocity controls on the right stick. - * Change the value until you are visually happy with the skew. Ensure your tune works with - * different translational and rotational magnitudes. If this reduces skew in teleop, it may - * improve auto. - */ - public void setAngularVelocityCompensation( - boolean useInTeleop, boolean useInAuto, double angularVelocityCoeff) { - if (!SwerveDriveTelemetry.isSimulation) { - angularVelocityCorrection = useInTeleop; - autonomousAngularVelocityCorrection = useInAuto; - angularVelocityCoefficient = angularVelocityCoeff; - } - } - - /** - * Correct for skew that worsens as angular velocity increases - * - * @param robotRelativeVelocity The chassis speeds to set the robot to achieve. - * @return {@link ChassisSpeeds} of the robot after angular velocity skew correction. - */ - public ChassisSpeeds angularVelocitySkewCorrection(ChassisSpeeds robotRelativeVelocity) { - var angularVelocity = - new Rotation2d( - imu.getYawAngularVelocity().in(RadiansPerSecond) * angularVelocityCoefficient); - if (angularVelocity.getRadians() != 0.0) { - ChassisSpeeds fieldRelativeVelocity = - ChassisSpeeds.fromRobotRelativeSpeeds(robotRelativeVelocity, getOdometryHeading()); - robotRelativeVelocity = - ChassisSpeeds.fromFieldRelativeSpeeds( - fieldRelativeVelocity, getOdometryHeading().plus(angularVelocity)); - } - return robotRelativeVelocity; - } - - /** - * Enable desired drive corrections - * - * @param robotRelativeVelocity The chassis speeds to set the robot to achieve. - * @param uesChassisDiscretize Correct chassis velocity using 254's correction. - * @param useAngularVelocitySkewCorrection Use the robot's angular velocity to correct for skew. - * @return The chassis speeds after optimizations. - */ - private ChassisSpeeds movementOptimizations( - ChassisSpeeds robotRelativeVelocity, - boolean uesChassisDiscretize, - boolean useAngularVelocitySkewCorrection) { - - if (useAngularVelocitySkewCorrection) { - robotRelativeVelocity = angularVelocitySkewCorrection(robotRelativeVelocity); - } - - // Thank you to Jared Russell FRC254 for Open Loop Compensation Code - // https://www.chiefdelphi.com/t/whitepaper-swerve-drive-skew-and-second-order-kinematics/416964/5 - if (uesChassisDiscretize) { - robotRelativeVelocity = - ChassisSpeeds.discretize(robotRelativeVelocity, discretizationdtSeconds); - } - - return robotRelativeVelocity; - } - - /** - * Convert a {@link ChassisSpeeds} to {@link SwerveModuleState[]} for use elsewhere. - * - * @param robotRelativeVelocity {@link ChassisSpeeds} velocity to use. - * @param optimize Perform chassis velocity correction or angular velocity correction. - * @return {@link SwerveModuleState[]} for use elsewhere. - */ - public SwerveModuleState[] toServeModuleStates( - ChassisSpeeds robotRelativeVelocity, boolean optimize) { - if (optimize) { - robotRelativeVelocity = - movementOptimizations( - robotRelativeVelocity, chassisVelocityCorrection, angularVelocityCorrection); - } - return kinematics.toSwerveModuleStates(robotRelativeVelocity); - } -} diff --git a/src/main/java/swervelib/SwerveDriveTest.java b/src/main/java/swervelib/SwerveDriveTest.java deleted file mode 100644 index 3e979a35..00000000 --- a/src/main/java/swervelib/SwerveDriveTest.java +++ /dev/null @@ -1,477 +0,0 @@ -package swervelib; - -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.DegreesPerSecond; -import static edu.wpi.first.units.Units.Meter; -import static edu.wpi.first.units.Units.Meters; -import static edu.wpi.first.units.Units.MetersPerSecond; -import static edu.wpi.first.units.Units.Seconds; -import static edu.wpi.first.units.Units.Volts; - -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.units.measure.MutAngle; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.units.measure.MutDistance; -import edu.wpi.first.units.measure.MutLinearVelocity; -import edu.wpi.first.units.measure.MutVoltage; -import edu.wpi.first.units.measure.Voltage; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.RobotController; -import edu.wpi.first.wpilibj.Timer; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import edu.wpi.first.wpilibj.sysid.SysIdRoutineLog; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.SubsystemBase; -import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; -import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Config; -import java.util.function.Supplier; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** Class to perform tests on the swerve drive. */ -public class SwerveDriveTest { - - /** Tracks the voltage being applied to a motor */ - private static final MutVoltage m_appliedVoltage = new MutVoltage(0, 0, Volts); - /** Tracks the distance travelled of a position motor */ - private static final MutDistance m_distance = new MutDistance(0, 0, Meter); - /** Tracks the velocity of a positional motor */ - private static final MutLinearVelocity m_velocity = new MutLinearVelocity(0, 9, MetersPerSecond); - /** Tracks the rotations of an angular motor */ - private static final MutAngle m_anglePosition = new MutAngle(0, 0, Degrees); - /** Tracks the velocity of an angular motor */ - private static final MutAngularVelocity m_angVelocity = - new MutAngularVelocity(0, 0, DegreesPerSecond); - - /** - * Set the angle of the modules to a given {@link Rotation2d} - * - * @param swerveDrive {@link SwerveDrive} to use. - * @param moduleAngle {@link Rotation2d} to set every module to. - */ - public static void angleModules(SwerveDrive swerveDrive, Rotation2d moduleAngle) { - for (SwerveModule swerveModule : swerveDrive.getModules()) { - swerveModule.getAngleMotor().setReference(moduleAngle.getDegrees(), 0); - } - } - - /** - * Power the drive motors for the swerve drive to a set duty cycle percentage. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param percentage Duty cycle percentage of voltage to send to drive motors. - */ - public static void powerDriveMotorsDutyCycle(SwerveDrive swerveDrive, double percentage) { - for (SwerveModule swerveModule : swerveDrive.getModules()) { - swerveModule.getDriveMotor().set(percentage); - } - } - - /** - * Power the angle motors for the swerve drive to a set percentage. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param percentage DutyCycle percentage to send to angle motors. - */ - public static void powerAngleMotorsDutyCycle(SwerveDrive swerveDrive, double percentage) { - for (SwerveModule swerveModule : swerveDrive.getModules()) { - swerveModule.getAngleMotor().set(percentage); - } - } - - /** - * Power the drive motors for the swerve drive to a set voltage. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param volts Voltage to send to drive motors. - */ - public static void powerDriveMotorsVoltage(SwerveDrive swerveDrive, double volts) { - for (SwerveModule swerveModule : swerveDrive.getModules()) { - swerveModule.getDriveMotor().setVoltage(volts); - } - } - - /** - * Power the angle motors for the swerve drive to a set voltage. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param volts Voltage to send to angle motors. - */ - public static void powerAngleMotorsVoltage(SwerveDrive swerveDrive, double volts) { - for (SwerveModule swerveModule : swerveDrive.getModules()) { - swerveModule.getAngleMotor().setVoltage(volts); - } - } - - /** - * Set the modules to center to 0. - * - * @param swerveDrive Swerve Drive to control. - */ - public static void centerModules(SwerveDrive swerveDrive) { - angleModules(swerveDrive, Rotation2d.fromDegrees(0)); - } - - /** - * Set the modules to their rotary position to allow running sysid and spinning the robot - * - * @param swerveDrive Swerve Drive to control. - */ - public static void setModulesToRotaryPosition(SwerveDrive swerveDrive) { - SwerveModuleState[] rotaryStates = - swerveDrive.kinematics.toSwerveModuleStates(new ChassisSpeeds(0, 0, 1)); - for (int i = 0; i < swerveDrive.getModules().length; i++) { - swerveDrive - .getModules()[i] - .getAngleMotor() - .setReference(rotaryStates[i].angle.getDegrees(), 0); - } - } - - /** - * Set the sim modules to center to 0 and power them to drive in a voltage. Calling this function - * in sim is equivalent to calling {@link #centerModules(SwerveDrive)} and {@link - * #powerDriveMotorsVoltage(SwerveDrive, double)} on a real robot. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param volts Voltage to send to drive motors. - * @param testWithSpinning - Whether to make the robot spin in place instead of driving in a - * straight line, true to make the robot spin, false to make the robot drive in straight line - */ - public static void runDriveMotorsCharacterizationOnSimModules( - SwerveDrive swerveDrive, double volts, boolean testWithSpinning) { - SwerveModuleState[] rotaryStates = - swerveDrive.kinematics.toSwerveModuleStates(new ChassisSpeeds(0, 0, 1)); - for (int i = 0; i < swerveDrive.getModules().length; i++) { - swerveDrive - .getModules()[i] - .getSimModule() - .runDriveMotorCharacterization( - testWithSpinning ? rotaryStates[i].angle : Rotation2d.kZero, volts); - } - } - - /** - * Set the sim modules to center to 0 and power them to drive in a voltage. Calling this function - * in sim is equivalent to calling {@link #centerModules(SwerveDrive)} and {@link - * #powerDriveMotorsVoltage(SwerveDrive, double)} on a real robot. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param volts Voltage to send to angle motors. - */ - public static void runAngleMotorsCharacterizationOnSimModules( - SwerveDrive swerveDrive, double volts) { - for (SwerveModule module : swerveDrive.getModules()) { - module.getSimModule().runAngleMotorCharacterization(volts); - } - } - - /** - * Find the minimum amount of power required to move the swerve drive motors. - * - * @param swerveDrive {@link SwerveDrive} to control. - * @param minMovement Minimum amount of movement to drive motors. - * @param testDelaySeconds Time in seconds for the motor to move. - * @param maxVolts The maximum voltage to send to drive motors. - * @return minimum voltage required. - */ - public static double findDriveMotorKV( - SwerveDrive swerveDrive, double minMovement, double testDelaySeconds, double maxVolts) { - double[] startingEncoders = new double[4]; - double kV = 0; - - SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); - SwerveModule[] modules = swerveDrive.getModules(); - for (int i = 0; i < modules.length; i++) { - startingEncoders[i] = Math.abs(modules[i].getDriveMotor().getPosition()); - } - - for (double kV_new = 0; kV_new < maxVolts; kV_new += 0.0001) { - - SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, kV); - boolean foundkV = false; - double startTimeSeconds = Timer.getFPGATimestamp(); - while ((Timer.getFPGATimestamp() - startTimeSeconds) < testDelaySeconds && !foundkV) { - for (int i = 0; i < modules.length; i++) { - if ((modules[i].getDriveMotor().getPosition() - startingEncoders[i]) > minMovement) { - foundkV = true; - break; - } - } - } - if (foundkV) { - SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); - kV = kV_new; - } - } - return kV; - } - - /** - * Find the coupling ratio for all modules. - * - * @param swerveDrive {@link SwerveDrive} to operate with. - * @param volts Voltage to send to angle motors to spin. - * @param automatic Attempt to automatically spin the modules. - * @return Average coupling ratio. - */ - public static double findCouplingRatio(SwerveDrive swerveDrive, double volts, boolean automatic) { - System.out.println("Stopping the Swerve Drive."); - SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); - SwerveDriveTest.powerAngleMotorsVoltage(swerveDrive, 0); - Timer.delay(1); - double couplingRatioSum = 0; - for (SwerveModule module : swerveDrive.getModules()) { - if (module.getAbsoluteEncoder() == null) { - throw new RuntimeException("Absolute encoders are required to find the coupling ratio."); - } - SwerveAbsoluteEncoder absoluteEncoder = module.getAbsoluteEncoder(); - if (absoluteEncoder.readingError) { - throw new RuntimeException("Absolute encoder encountered a reading error please debug."); - } - System.out.println("Fetching the current absolute encoder and drive encoder position."); - module.getAngleMotor().setVoltage(0); - Timer.delay(1); - Rotation2d startingAbsoluteEncoderPosition = - Rotation2d.fromDegrees(absoluteEncoder.getAbsolutePosition()); - double driveEncoderPositionRotations = - module.getDriveMotor().getPosition() - / module.configuration.conversionFactors.drive.factor; - if (automatic) { - module.getAngleMotor().setVoltage(volts); - Timer.delay(0.01); - System.out.println("Rotating the module 360 degrees"); - while (!Rotation2d.fromDegrees(absoluteEncoder.getAbsolutePosition()) - .equals(startingAbsoluteEncoderPosition)) - ; - module.getAngleMotor().setVoltage(0); - } else { - DriverStation.reportWarning( - "Spin the " - + module.configuration.name - + " module 360 degrees now, you have 1 minute.\n", - false); - Timer.delay(60); - } - double couplingRatio = - (module.getDriveMotor().getPosition() - / module.configuration.conversionFactors.drive.factor) - - driveEncoderPositionRotations; - DriverStation.reportWarning( - module.configuration.name + " Coupling Ratio: " + couplingRatio, false); - couplingRatioSum += couplingRatio; - } - DriverStation.reportWarning("Average Coupling Ratio: " + (couplingRatioSum / 4.0), false); - return (couplingRatioSum / 4.0); - } - - /** - * Creates a SysIdRoutine.Config with a custom final timeout - * - * @param timeout - the most a SysIdRoutine should run - * @return A custom SysIdRoutine.Config - */ - public static Config createConfigCustomTimeout(double timeout) { - return new Config(null, null, Seconds.of(timeout)); - } - - /** - * Logs output, position and velocuty info form the drive motor to the SysIdRoutineLog
- * Although SysIdRoutine expects to be logging Voltage, this function logs in Duty-Cycle (percent - * output) because it results in correctly adjusted values in the analysis for use in this - * library. - * - * @param module - the swerve module being logged - * @param log - the logger - */ - public static void logDriveMotorDutyCycle(SwerveModule module, SysIdRoutineLog log) { - logDriveMotorActivity( - module, - log, - () -> module.getDriveMotor().getVoltage() / RobotController.getBatteryVoltage()); - } - - /** - * Logs voltage, position and velocuty info form the drive motor to the SysIdRoutineLog - * - * @param module - the swerve module being logged - * @param log - the logger - */ - public static void logDriveMotorVoltage(SwerveModule module, SysIdRoutineLog log) { - logDriveMotorActivity(module, log, () -> module.getDriveMotor().getVoltage()); - } - - /** - * Logs power, position and velocuty info form the drive motor to the SysIdRoutineLog - * - * @param module - the swerve module being logged - * @param log - the logger - * @param powerSupplied - a functional supplier of the power to be logged - */ - public static void logDriveMotorActivity( - SwerveModule module, SysIdRoutineLog log, Supplier powerSupplied) { - double power = powerSupplied.get(); - double distance = module.getPosition().distanceMeters; - double velocity = module.getDriveMotor().getVelocity(); - SmartDashboard.putNumber( - "swerve/modules/" + module.configuration.name + "/SysId Drive Power", power); - SmartDashboard.putNumber( - "swerve/modules/" + module.configuration.name + "/SysId Drive Position", distance); - SmartDashboard.putNumber( - "swerve/modules/" + module.configuration.name + "/SysId Drive Velocity", velocity); - log.motor("drive-" + module.configuration.name) - .voltage(m_appliedVoltage.mut_replace(power, Volts)) - .linearPosition(m_distance.mut_replace(distance, Meters)) - .linearVelocity(m_velocity.mut_replace(velocity, MetersPerSecond)); - } - - /** - * Sets up the SysId runner and logger for the drive motors - * - * @param config - The SysIdRoutine.Config to use - * @param swerveSubsystem - the subsystem to add to requirements - * @param swerveDrive - the SwerveDrive from which to access motor info - * @param maxVolts - The maximum voltage that should be applied to the drive motors. - * @param testWithSpinning - Whether to make the robot spin in place instead of driving in a - * straight line, true to make the robot spin, false to make the robot drive in straight line - * @return A SysIdRoutine runner - */ - public static SysIdRoutine setDriveSysIdRoutine( - Config config, - SubsystemBase swerveSubsystem, - SwerveDrive swerveDrive, - double maxVolts, - boolean testWithSpinning) { - return new SysIdRoutine( - config, - new SysIdRoutine.Mechanism( - (Voltage voltage) -> { - if (!SwerveDriveTelemetry.isSimulation) { - if (testWithSpinning) { - SwerveDriveTest.setModulesToRotaryPosition(swerveDrive); - } else { - SwerveDriveTest.centerModules(swerveDrive); - } - SwerveDriveTest.powerDriveMotorsVoltage( - swerveDrive, Math.min(voltage.in(Volts), maxVolts)); - } else { - SwerveDriveTest.runDriveMotorsCharacterizationOnSimModules( - swerveDrive, voltage.in(Volts), testWithSpinning); - } - }, - log -> { - for (SwerveModule module : swerveDrive.getModules()) { - logDriveMotorVoltage(module, log); - } - }, - swerveSubsystem)); - } - - /** - * Logs info about the angle motor to the SysIdRoutineLog.
- * Although SysIdRoutine expects to be logging Voltage, this function logs in Duty-Cycle (percent - * output) because it results in correctly adjusted values in the analysis for use in this - * library. - * - * @param module - the swerve module being logged - * @param log - the logger - */ - public static void logAngularMotorDutyCycle(SwerveModule module, SysIdRoutineLog log) { - logAngularMotorActivity( - module, - log, - () -> module.getAngleMotor().getVoltage() / RobotController.getBatteryVoltage()); - } - - /** - * Logs info about the angle motor to the SysIdRoutineLog - * - * @param module - the swerve module being logged - * @param log - the logger - */ - public static void logAngularMotorVoltage(SwerveModule module, SysIdRoutineLog log) { - logAngularMotorActivity(module, log, () -> module.getAngleMotor().getVoltage()); - } - - /** - * Logs info about the angle motor to the SysIdRoutineLog - * - * @param module - the swerve module being logged - * @param log - the logger - * @param powerSupplied - a functional supplier of the power to be logged - */ - public static void logAngularMotorActivity( - SwerveModule module, SysIdRoutineLog log, Supplier powerSupplied) { - double power = powerSupplied.get(); - double angle = module.getAngleMotor().getPosition(); - double velocity = module.getAngleMotor().getVelocity(); - SmartDashboard.putNumber( - "swerve/modules/" + module.configuration.name + "/SysId Angle Power", power); - SmartDashboard.putNumber( - "swerve/modules/" + module.configuration.name + "/SysId Angle Position", angle); - SmartDashboard.putNumber( - "swerve/modules/" + module.configuration.name + "/SysId Absolute Encoder Velocity", - velocity); - log.motor("angle-" + module.configuration.name) - .voltage(m_appliedVoltage.mut_replace(power, Volts)) - .angularPosition(m_anglePosition.mut_replace(angle, Degrees)) - .angularVelocity(m_angVelocity.mut_replace(velocity, DegreesPerSecond)); - } - - /** - * Sets up the SysId runner and logger for the angle motors - * - * @param config - The SysIdRoutine.Config to use - * @param swerveSubsystem - the subsystem to add to requirements - * @param swerveDrive - the SwerveDrive from which to access motor info - * @return A SysIdRoutineRunner - */ - public static SysIdRoutine setAngleSysIdRoutine( - Config config, SubsystemBase swerveSubsystem, SwerveDrive swerveDrive) { - return new SysIdRoutine( - config, - new SysIdRoutine.Mechanism( - (Voltage voltage) -> { - if (!SwerveDriveTelemetry.isSimulation) { - SwerveDriveTest.powerAngleMotorsVoltage(swerveDrive, voltage.in(Volts)); - SwerveDriveTest.powerDriveMotorsVoltage(swerveDrive, 0); - } else { - SwerveDriveTest.runAngleMotorsCharacterizationOnSimModules( - swerveDrive, voltage.in(Volts)); - } - }, - log -> { - for (SwerveModule module : swerveDrive.getModules()) { - logAngularMotorVoltage(module, log); - } - }, - swerveSubsystem)); - } - - /** - * Creates a command that can be mapped to a button or other trigger. Delays can be set to - * customize the length of each part of the SysId Routine - * - * @param sysIdRoutine - The Sys Id routine runner - * @param delay - seconds between each portion to allow motors to spin down, etc... - * @param quasiTimeout - seconds to run the Quasistatic routines, so robot doesn't get too far - * @param dynamicTimeout - seconds to run the Dynamic routines, 2-3 secs should be enough - * @return A command that can be mapped to a button or other trigger - */ - public static Command generateSysIdCommand( - SysIdRoutine sysIdRoutine, double delay, double quasiTimeout, double dynamicTimeout) { - return sysIdRoutine - .quasistatic(SysIdRoutine.Direction.kForward) - .withTimeout(quasiTimeout) - .andThen(Commands.waitSeconds(delay)) - .andThen( - sysIdRoutine.quasistatic(SysIdRoutine.Direction.kReverse).withTimeout(quasiTimeout)) - .andThen(Commands.waitSeconds(delay)) - .andThen(sysIdRoutine.dynamic(SysIdRoutine.Direction.kForward).withTimeout(dynamicTimeout)) - .andThen(Commands.waitSeconds(delay)) - .andThen(sysIdRoutine.dynamic(SysIdRoutine.Direction.kReverse).withTimeout(dynamicTimeout)); - } -} diff --git a/src/main/java/swervelib/SwerveInputStream.java b/src/main/java/swervelib/SwerveInputStream.java deleted file mode 100644 index 7c848841..00000000 --- a/src/main/java/swervelib/SwerveInputStream.java +++ /dev/null @@ -1,914 +0,0 @@ -package swervelib; - -import edu.wpi.first.math.MathUtil; -import edu.wpi.first.math.Nat; -import edu.wpi.first.math.Vector; -import edu.wpi.first.math.controller.ProfiledPIDController; -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.numbers.N2; -import edu.wpi.first.math.trajectory.TrapezoidProfile.State; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.DriverStation.Alliance; -import edu.wpi.first.wpilibj.XboxController; -import java.util.Optional; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; -import swervelib.math.SwerveMath; - -/** - * Helper class to easily transform Controller inputs into workable Chassis speeds. Intended to - * easily create an interface that generates {@link ChassisSpeeds} from {@link XboxController} - * - *


- * Inspired by SciBorgs FRC 1155.
- * Example: - * - *

{@code
- * XboxController driverXbox = new XboxController(0);
- *
- * SwerveInputStream driveAngularVelocity = SwerveInputStream.of(drivebase.getSwerveDrive(),
- *                                                               () -> driverXbox.getLeftY() * -1,
- *                                                               () -> driverXbox.getLeftX() * -1) // Axis which give the desired translational angle and speed.
- *                                                           .withControllerRotationAxis(driverXbox::getRightX) // Axis which give the desired angular velocity.
- *                                                           .deadband(0.01)                  // Controller deadband
- *                                                           .scaleTranslation(0.8)           // Scaled controller translation axis
- *                                                           .allianceRelativeControl(true);  // Alliance relative controls.
- *
- * SwerveInputStream driveDirectAngle = driveAngularVelocity.copy()  // Copy the stream so further changes do not affect driveAngularVelocity
- *                                                          .withControllerHeadingAxis(driverXbox::getRightX,
- *                                                                                     driverXbox::getRightY) // Axis which give the desired heading angle using trigonometry.
- *                                                          .headingWhile(true); // Enable heading based control.
- * }
- */ -public class SwerveInputStream implements Supplier { - - /** Translation suppliers. */ - private final DoubleSupplier controllerTranslationX; - /** Translational supplier. */ - private final DoubleSupplier controllerTranslationY; - /** {@link SwerveDrive} object for transformations. */ - private final SwerveDrive swerveDrive; - /** Rotation supplier as angular velocity. */ - private Optional controllerOmega = Optional.empty(); - /** Controller supplier as heading. */ - private Optional controllerHeadingX = Optional.empty(); - /** Controller supplier as heading. */ - private Optional controllerHeadingY = Optional.empty(); - /** Axis deadband for the controller. */ - private Optional axisDeadband = Optional.empty(); - /** Translational axis scalar value, should be between (0, 1]. */ - private Optional translationAxisScale = Optional.empty(); - /** Angular velocity axis scalar value, should be between (0, 1] */ - private Optional omegaAxisScale = Optional.empty(); - /** Target to aim at. */ - private Optional aimTarget = Optional.empty(); - /** Target {@link Supplier} to drive towards when driveToPose is enabled. */ - private Optional> driveToPose = Optional.empty(); - /** {@link ProfiledPIDController} for the translation while driving to a pose. Units are m/s */ - private Optional driveToPoseTranslationPIDController = Optional.empty(); - /** - * {@link ProfiledPIDController} for the Rotational axis while driving to a pose. Units are m/s - */ - private Optional driveToPoseOmegaPIDController = Optional.empty(); - /** Output {@link ChassisSpeeds} based on heading while this is True. */ - private Optional headingEnabled = Optional.empty(); - /** Locked heading for {@link SwerveInputMode#TRANSLATION_ONLY} */ - private Optional lockedHeading = Optional.empty(); - /** Output {@link ChassisSpeeds} based on aim while this is True. */ - private Optional aimEnabled = Optional.empty(); - /** Output {@link ChassisSpeeds} to move to a specific {@link Pose2d}. */ - private Optional driveToPoseEnabled = Optional.empty(); - /** Maintain current heading and drive without rotating, ideally. */ - private Optional translationOnlyEnabled = Optional.empty(); - /** Cube the translation magnitude from the controller. */ - private Optional translationCube = Optional.empty(); - /** Cube the angular velocity axis from the controller. */ - private Optional omegaCube = Optional.empty(); - /** Robot relative oriented output expected. */ - private Optional robotRelative = Optional.empty(); - /** Field oriented chassis output is relative to your current alliance. */ - private Optional allianceRelative = Optional.empty(); - /** Heading offset enable state. */ - private Optional translationHeadingOffsetEnabled = Optional.empty(); - /** Heading offset to apply during heading based control. */ - private Optional translationHeadingOffset = Optional.empty(); - /** {@link SwerveController} for simple control over heading. */ - private SwerveController swerveController = null; - /** Current {@link SwerveInputMode} to use. */ - private SwerveInputMode currentMode = SwerveInputMode.ANGULAR_VELOCITY; - - /** - * Create a {@link SwerveInputStream} for an easy way to generate {@link ChassisSpeeds} from a - * driver controller. - * - * @param drive {@link SwerveDrive} object for transformation. - * @param x Translation X input in range of [-1, 1] - * @param y Translation Y input in range of [-1, 1] - */ - private SwerveInputStream(SwerveDrive drive, DoubleSupplier x, DoubleSupplier y) { - controllerTranslationX = x; - controllerTranslationY = y; - swerveDrive = drive; - } - - /** - * Create a {@link SwerveInputStream} for an easy way to generate {@link ChassisSpeeds} from a - * driver controller. - * - * @param drive {@link SwerveDrive} object for transformation. - * @param x Translation X input in range of [-1, 1] - * @param y Translation Y input in range of [-1, 1] - * @param rot Rotation input in range of [-1, 1] - */ - public SwerveInputStream( - SwerveDrive drive, DoubleSupplier x, DoubleSupplier y, DoubleSupplier rot) { - this(drive, x, y); - controllerOmega = Optional.of(rot); - } - - /** - * Create a {@link SwerveInputStream} for an easy way to generate {@link ChassisSpeeds} from a - * driver controller. - * - * @param drive {@link SwerveDrive} object for transformation. - * @param x Translation X input in range of [-1, 1] - * @param y Translation Y input in range of [-1, 1] - * @param headingX Heading X input in range of [-1, 1] - * @param headingY Heading Y input in range of [-1, 1] - */ - public SwerveInputStream( - SwerveDrive drive, - DoubleSupplier x, - DoubleSupplier y, - DoubleSupplier headingX, - DoubleSupplier headingY) { - this(drive, x, y); - controllerHeadingX = Optional.of(headingX); - controllerHeadingY = Optional.of(headingY); - } - - /** - * Create basic {@link SwerveInputStream} without any rotation components. - * - * @param drive {@link SwerveDrive} object for transformation. - * @param x {@link DoubleSupplier} of the translation X axis of the controller joystick to use. - * @param y {@link DoubleSupplier} of the translation X axis of the controller joystick to use. - * @return {@link SwerveInputStream} to use as you see fit. - */ - public static SwerveInputStream of(SwerveDrive drive, DoubleSupplier x, DoubleSupplier y) { - return new SwerveInputStream(drive, x, y); - } - - /** - * Copy the {@link SwerveInputStream} object. - * - * @return Clone of current {@link SwerveInputStream} - */ - public SwerveInputStream copy() { - SwerveInputStream newStream = - new SwerveInputStream(swerveDrive, controllerTranslationX, controllerTranslationY); - newStream.controllerOmega = controllerOmega; - newStream.controllerHeadingX = controllerHeadingX; - newStream.controllerHeadingY = controllerHeadingY; - newStream.axisDeadband = axisDeadband; - newStream.translationAxisScale = translationAxisScale; - newStream.omegaAxisScale = omegaAxisScale; - newStream.driveToPose = driveToPose; - newStream.driveToPoseTranslationPIDController = driveToPoseTranslationPIDController; - newStream.driveToPoseOmegaPIDController = driveToPoseOmegaPIDController; - newStream.aimTarget = aimTarget; - newStream.headingEnabled = headingEnabled; - newStream.aimEnabled = aimEnabled; - newStream.driveToPoseEnabled = driveToPoseEnabled; - newStream.currentMode = currentMode; - newStream.translationOnlyEnabled = translationOnlyEnabled; - newStream.lockedHeading = lockedHeading; - newStream.swerveController = swerveController; - newStream.omegaCube = omegaCube; - newStream.translationCube = translationCube; - newStream.robotRelative = robotRelative; - newStream.allianceRelative = allianceRelative; - newStream.translationHeadingOffsetEnabled = translationHeadingOffsetEnabled; - newStream.translationHeadingOffset = translationHeadingOffset; - return newStream; - } - - /** - * Set the stream to output robot relative {@link ChassisSpeeds} - * - * @param enabled Robot-Relative {@link ChassisSpeeds} output. - * @return self - */ - public SwerveInputStream robotRelative(BooleanSupplier enabled) { - robotRelative = Optional.of(enabled); - return this; - } - - /** - * Set the stream to output robot relative {@link ChassisSpeeds} - * - * @param enabled Robot-Relative {@link ChassisSpeeds} output. - * @return self - */ - public SwerveInputStream robotRelative(boolean enabled) { - robotRelative = enabled ? Optional.of(() -> enabled) : Optional.empty(); - return this; - } - - /** - * Drive to a given pose with the provided {@link ProfiledPIDController}s - * - * @param pose {@link Supplier} for ease of use. - * @param xPIDController PID controller for the translational axis, units are m/s. - * @param omegaPIDController PID Controller for rotational axis, units are rad/s. - * @return self - */ - public SwerveInputStream driveToPose( - Supplier pose, - ProfiledPIDController xPIDController, - ProfiledPIDController omegaPIDController) { - omegaPIDController.reset(swerveDrive.getPose().getRotation().getRadians()); - xPIDController.reset( - swerveDrive.getPose().getTranslation().getDistance(pose.get().getTranslation())); - omegaPIDController.enableContinuousInput(-Math.PI, Math.PI); - xPIDController.setGoal(new State(0, 0)); - driveToPose = Optional.of(pose); - driveToPoseTranslationPIDController = Optional.of(xPIDController); - driveToPoseOmegaPIDController = Optional.of(omegaPIDController); - return this; - } - - /** - * Enable driving to the target pose. - * - * @param enabled Enable state of drive to pose. - * @return self. - */ - public SwerveInputStream driveToPoseEnabled(BooleanSupplier enabled) { - driveToPoseEnabled = Optional.of(enabled); - return this; - } - - /** - * Enable driving to the target pose. - * - * @param enabled Enable state of drive to pose. - * @return self. - */ - public SwerveInputStream driveToPoseEnabled(boolean enabled) { - driveToPoseEnabled = enabled ? Optional.of(() -> enabled) : Optional.empty(); - Pose2d swervePose = swerveDrive.getPose(); - // driveToPoseXPIDController.ifPresent(profiledPIDController -> - // profiledPIDController.reset(swervePose.getX())); - // driveToPoseYPIDController.ifPresent(profiledPIDController -> - // profiledPIDController.reset(swervePose.getY())); - // driveToPoseOmegaPIDController.ifPresent(profiledPIDController -> - // profiledPIDController.reset(swervePose.getRotation() - // - // .getRadians())); - return this; - } - - /** - * Heading offset enabled boolean supplier. - * - * @param enabled Enable state - * @return self - */ - public SwerveInputStream translationHeadingOffset(BooleanSupplier enabled) { - translationHeadingOffsetEnabled = Optional.of(enabled); - return this; - } - - /** - * Heading offset enable - * - * @param enabled Enable state - * @return self - */ - public SwerveInputStream translationHeadingOffset(boolean enabled) { - translationHeadingOffsetEnabled = enabled ? Optional.of(() -> enabled) : Optional.empty(); - return this; - } - - /** - * Set the heading offset angle. - * - * @param angle {@link Rotation2d} offset to apply - * @return self - */ - public SwerveInputStream translationHeadingOffset(Rotation2d angle) { - translationHeadingOffset = Optional.of(angle); - return this; - } - - /** - * Modify the output {@link ChassisSpeeds} so that it is always relative to your alliance. - * - * @param enabled Alliance aware {@link ChassisSpeeds} output. - * @return self - */ - public SwerveInputStream allianceRelativeControl(BooleanSupplier enabled) { - allianceRelative = Optional.of(enabled); - return this; - } - - /** - * Modify the output {@link ChassisSpeeds} so that it is always relative to your alliance. - * - * @param enabled Alliance aware {@link ChassisSpeeds} output. - * @return self - */ - public SwerveInputStream allianceRelativeControl(boolean enabled) { - allianceRelative = enabled ? Optional.of(() -> enabled) : Optional.empty(); - return this; - } - - /** - * Cube the angular velocity controller axis for a non-linear controls scheme. - * - * @param enabled Enabled state for the stream. - * @return self. - */ - public SwerveInputStream cubeRotationControllerAxis(BooleanSupplier enabled) { - omegaCube = Optional.of(enabled); - return this; - } - - /** - * Cube the angular velocity controller axis for a non-linear controls scheme. - * - * @param enabled Enabled state for the stream. - * @return self. - */ - public SwerveInputStream cubeRotationControllerAxis(boolean enabled) { - omegaCube = Optional.of(() -> enabled); - return this; - } - - /** - * Cube the translation axis magnitude for a non-linear control scheme. - * - * @param enabled Enabled state for the stream - * @return self - */ - public SwerveInputStream cubeTranslationControllerAxis(BooleanSupplier enabled) { - translationOnlyEnabled = Optional.of(enabled); - return this; - } - - /** - * Cube the translation axis magnitude for a non-linear control scheme - * - * @param enabled Enabled state for the stream - * @return self - */ - public SwerveInputStream cubeTranslationControllerAxis(boolean enabled) { - translationCube = enabled ? Optional.of(() -> enabled) : Optional.empty(); - return this; - } - - /** - * Add a rotation axis for Angular Velocity control - * - * @param rot Rotation axis with values from [-1, 1] - * @return self - */ - public SwerveInputStream withControllerRotationAxis(DoubleSupplier rot) { - controllerOmega = Optional.of(rot); - return this; - } - - /** - * Add heading axis for Heading based control. - * - * @param headingX Heading X axis with values from [-1, 1] - * @param headingY Heading Y axis with values from [-1, 1] - * @return self - */ - public SwerveInputStream withControllerHeadingAxis( - DoubleSupplier headingX, DoubleSupplier headingY) { - controllerHeadingX = Optional.of(headingX); - controllerHeadingY = Optional.of(headingY); - return this; - } - - /** - * Set a deadband for all controller axis. - * - * @param deadband Deadband to set, should be between [0, 1) - * @return self - */ - public SwerveInputStream deadband(double deadband) { - axisDeadband = deadband == 0 ? Optional.empty() : Optional.of(deadband); - return this; - } - - /** - * Scale the translation axis for {@link SwerveInputStream} by a constant scalar value. - * - * @param scaleTranslation Translation axis scalar value. (0, 1] - * @return this - */ - public SwerveInputStream scaleTranslation(double scaleTranslation) { - translationAxisScale = scaleTranslation == 0 ? Optional.empty() : Optional.of(scaleTranslation); - return this; - } - - /** - * Scale the rotation axis input for {@link SwerveInputStream} to reduce the range in which they - * operate. - * - * @param scaleRotation Angular velocity axis scalar value. (0, 1] - * @return this - */ - public SwerveInputStream scaleRotation(double scaleRotation) { - omegaAxisScale = scaleRotation == 0 ? Optional.empty() : Optional.of(scaleRotation); - return this; - } - - /** - * Output {@link ChassisSpeeds} based on heading while the supplier is True. - * - * @param trigger Supplier to use. - * @return this. - */ - public SwerveInputStream headingWhile(BooleanSupplier trigger) { - headingEnabled = Optional.of(trigger); - return this; - } - - /** - * Set the heading enable state. - * - * @param headingState Heading enabled state. - * @return this - */ - public SwerveInputStream headingWhile(boolean headingState) { - if (headingState) { - headingEnabled = Optional.of(() -> true); - } else { - headingEnabled = Optional.empty(); - } - return this; - } - - /** - * Aim the {@link SwerveDrive} at this pose while driving. - * - * @param aimTarget {@link Pose2d} to point at. - * @return this - */ - public SwerveInputStream aim(Pose2d aimTarget) { - this.aimTarget = aimTarget.equals(Pose2d.kZero) ? Optional.empty() : Optional.of(aimTarget); - return this; - } - - /** - * Enable aiming while the trigger is true. - * - * @param trigger When True will enable aiming at the current target. - * @return this. - */ - public SwerveInputStream aimWhile(BooleanSupplier trigger) { - aimEnabled = Optional.of(trigger); - return this; - } - - /** - * Enable aiming while the trigger is true. - * - * @param trigger When True will enable aiming at the current target. - * @return this. - */ - public SwerveInputStream aimWhile(boolean trigger) { - if (trigger) { - aimEnabled = Optional.of(() -> true); - } else { - aimEnabled = Optional.empty(); - } - return this; - } - - /** - * Enable locking of rotation and only translating, overrides everything. - * - * @param trigger Translation only while returns true. - * @return this - */ - public SwerveInputStream translationOnlyWhile(BooleanSupplier trigger) { - translationOnlyEnabled = Optional.of(trigger); - return this; - } - - /** - * Enable locking of rotation and only translating, overrides everything. - * - * @param translationState Translation only if true. - * @return this - */ - public SwerveInputStream translationOnlyWhile(boolean translationState) { - if (translationState) { - translationOnlyEnabled = Optional.of(() -> true); - } else { - translationOnlyEnabled = Optional.empty(); - } - return this; - } - - /** - * Find {@link SwerveInputMode} based off existing parameters of the {@link SwerveInputStream} - * - * @return The calculated {@link SwerveInputMode}, defaults to {@link - * SwerveInputMode#ANGULAR_VELOCITY}. - */ - private SwerveInputMode findMode() { - if (driveToPoseEnabled.isPresent() && driveToPoseEnabled.get().getAsBoolean()) { - if (driveToPose.isPresent()) { - if (driveToPoseOmegaPIDController.isPresent() - && driveToPoseTranslationPIDController.isPresent()) { - return SwerveInputMode.DRIVE_TO_POSE; - } - System.out.println("Drive to pose present"); - DriverStation.reportError("Drive to pose not supplied with pid controllers.", false); - } - DriverStation.reportError("Drive to pose enabled without supplier present.", false); - } else if (translationOnlyEnabled.isPresent() && translationOnlyEnabled.get().getAsBoolean()) { - return SwerveInputMode.TRANSLATION_ONLY; - } else if (aimEnabled.isPresent() && aimEnabled.get().getAsBoolean()) { - if (aimTarget.isPresent()) { - return SwerveInputMode.AIM; - } else { - DriverStation.reportError( - "Attempting to enter AIM mode without target, please use SwerveInputStream.aim() to select a target first!", - false); - } - } else if (headingEnabled.isPresent() && headingEnabled.get().getAsBoolean()) { - if (controllerHeadingX.isPresent() && controllerHeadingY.isPresent()) { - return SwerveInputMode.HEADING; - } else { - DriverStation.reportError( - "Attempting to enter HEADING mode without heading axis, please use SwerveInputStream.withControllerHeadingAxis to add heading axis!", - false); - } - } else if (controllerOmega.isEmpty()) { - DriverStation.reportError( - "Attempting to enter ANGULAR_VELOCITY mode without a rotation axis, please use SwerveInputStream.withControllerRotationAxis to add angular velocity axis!", - false); - return SwerveInputMode.TRANSLATION_ONLY; - } - return SwerveInputMode.ANGULAR_VELOCITY; - } - - /** - * Transition smoothly from one mode to another. - * - * @param newMode New mode to transition too. - */ - private void transitionMode(SwerveInputMode newMode) { - // Handle removing of current mode. - switch (currentMode) { - case TRANSLATION_ONLY -> { - lockedHeading = Optional.empty(); - break; - } - case ANGULAR_VELOCITY, HEADING, AIM -> { - // Do nothing - break; - } - case DRIVE_TO_POSE -> { - break; - } - } - - // Transitioning to new mode - switch (newMode) { - case TRANSLATION_ONLY -> { - lockedHeading = Optional.of(swerveDrive.getOdometryHeading()); - break; - } - case ANGULAR_VELOCITY -> { - if (swerveDrive.headingCorrection) { - swerveDrive.setHeadingCorrection(false); - } - break; - } - case HEADING, AIM -> { - // Do nothing - break; - } - case DRIVE_TO_POSE -> { - if (swerveDrive.headingCorrection) { - swerveDrive.setHeadingCorrection(false); - } - } - } - } - - /** - * Apply the deadband if it exists. - * - * @param axisValue Axis value to apply the deadband too. - * @return axis value with deadband, else axis value straight. - */ - private double applyDeadband(double axisValue) { - if (axisDeadband.isPresent()) { - return MathUtil.applyDeadband(axisValue, axisDeadband.get()); - } - return axisValue; - } - - /** - * Apply the scalar value if it exists. - * - * @param axisValue Axis value to apply teh scalar too. - * @return Axis value scaled by scalar value. - */ - private double applyRotationalScalar(double axisValue) { - if (omegaAxisScale.isPresent()) { - return axisValue * omegaAxisScale.get(); - } - return axisValue; - } - - /** - * Scale the translational axis by the {@link SwerveInputStream#translationAxisScale} if it - * exists. - * - * @param xAxis X axis to scale. - * @param yAxis Y axis to scale. - * @return Scaled {@link Translation2d} - */ - private Translation2d applyTranslationScalar(double xAxis, double yAxis) { - if (translationAxisScale.isPresent()) { - - return SwerveMath.scaleTranslation( - new Translation2d(xAxis, yAxis), translationAxisScale.get()); - } - return new Translation2d(xAxis, yAxis); - } - - /** - * Apply the cube transformation on the given {@link Translation2d} - * - * @param translation {@link Translation2d} representing controller input - * @return Cubed {@link Translation2d} if the {@link SwerveInputStream#translationCube} is - * present. - */ - private Translation2d applyTranslationCube(Translation2d translation) { - if (translationCube.isPresent() && translationCube.get().getAsBoolean()) { - return SwerveMath.cubeTranslation(translation); - } - return translation; - } - - /** - * Apply the cube transformation on the given rotation controller axis - * - * @param rotationAxis Rotation controller axis to cube. - * @return Cubed axis value if the {@link SwerveInputStream#omegaCube} is present. - */ - private double applyOmegaCube(double rotationAxis) { - if (omegaCube.isPresent() && omegaCube.get().getAsBoolean()) { - return Math.pow(rotationAxis, 3); - } - return rotationAxis; - } - - /** - * Change {@link ChassisSpeeds} from robot relative if enabled. - * - * @param fieldRelativeSpeeds Field or robot relative speeds to translate into robot-relative - * speeds. - * @return Field relative {@link ChassisSpeeds}. - */ - private ChassisSpeeds applyRobotRelativeTranslation(ChassisSpeeds fieldRelativeSpeeds) { - if (robotRelative.isPresent() && robotRelative.get().getAsBoolean()) { - return ChassisSpeeds.fromRobotRelativeSpeeds( - fieldRelativeSpeeds, swerveDrive.getOdometryHeading()); - } - return fieldRelativeSpeeds; - } - - /** - * Apply alliance aware translation which flips the {@link Translation2d} if the robot is on the - * Blue alliance. - * - * @param fieldRelativeTranslation Field-relative {@link Translation2d} to flip. - * @return Alliance-oriented {@link Translation2d} - */ - private Translation2d applyAllianceAwareTranslation(Translation2d fieldRelativeTranslation) { - if (allianceRelative.isPresent() && allianceRelative.get().getAsBoolean()) { - if (robotRelative.isPresent() && robotRelative.get().getAsBoolean()) { - if (driveToPoseEnabled.isPresent() && driveToPoseEnabled.get().getAsBoolean()) { - return fieldRelativeTranslation; - } - throw new RuntimeException( - "Cannot use robot oriented control with Alliance aware movement!"); - } - if (DriverStation.getAlliance().isPresent() - && DriverStation.getAlliance().get() == Alliance.Red) { - return fieldRelativeTranslation.rotateBy(Rotation2d.k180deg); - } - } - return fieldRelativeTranslation; - } - - /** - * Adds offset to translation if one is set. - * - * @param speeds {@link ChassisSpeeds} to offset - * @return Offsetted {@link ChassisSpeeds} - */ - private ChassisSpeeds applyTranslationHeadingOffset(ChassisSpeeds speeds) { - if (translationHeadingOffsetEnabled.isPresent() - && translationHeadingOffsetEnabled.get().getAsBoolean()) { - if (translationHeadingOffset.isPresent()) { - Translation2d speedsTranslation = - new Translation2d(speeds.vxMetersPerSecond, speeds.vyMetersPerSecond) - .rotateBy(translationHeadingOffset.get()); - return new ChassisSpeeds( - speedsTranslation.getX(), speedsTranslation.getY(), speeds.omegaRadiansPerSecond); - } - } - return speeds; - } - - /** - * When the {@link SwerveInputStream} is in {@link SwerveInputMode#DRIVE_TO_POSE} this function - * will return if the robot is at the desired pose within the defined tolerance. - * - * @param toleranceMeters Tolerance in meters. - * @return At target pose, true if current mode is not {@link SwerveInputMode#DRIVE_TO_POSE} and - * no pose supplier has been given. - */ - public boolean atTargetPose(double toleranceMeters) { - if (currentMode != SwerveInputMode.DRIVE_TO_POSE) { - DriverStation.reportError( - "SwerveInputStream.atTargetPose called while not set to DriveToPose.", false); - if (!driveToPose.isPresent()) { - return true; - } - } - if (driveToPose.isPresent()) { - Pose2d targetPose = driveToPose.get().get(); - return swerveDrive.getPose().getTranslation().getDistance(targetPose.getTranslation()) - <= toleranceMeters; - } - return true; - } - - /** - * Gets a {@link ChassisSpeeds} - * - * @return {@link ChassisSpeeds} - */ - @Override - public ChassisSpeeds get() { - double maximumChassisVelocity = swerveDrive.getMaximumChassisVelocity(); - Translation2d scaledTranslation = - applyTranslationScalar( - applyDeadband(controllerTranslationX.getAsDouble()), - applyDeadband(controllerTranslationY.getAsDouble())); - scaledTranslation = applyTranslationCube(scaledTranslation); - scaledTranslation = applyAllianceAwareTranslation(scaledTranslation); - - double vxMetersPerSecond = scaledTranslation.getX() * maximumChassisVelocity; - double vyMetersPerSecond = scaledTranslation.getY() * maximumChassisVelocity; - double omegaRadiansPerSecond = 0; - ChassisSpeeds speeds = new ChassisSpeeds(); - - SwerveInputMode newMode = findMode(); - // Handle transitions here. - if (currentMode != newMode) { - transitionMode(newMode); - } - if (swerveController == null) { - swerveController = swerveDrive.getSwerveController(); - } - switch (newMode) { - case TRANSLATION_ONLY -> { - omegaRadiansPerSecond = - swerveController.headingCalculate( - swerveDrive.getOdometryHeading().getRadians(), lockedHeading.get().getRadians()); - speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); - break; - } - case ANGULAR_VELOCITY -> { - omegaRadiansPerSecond = - applyOmegaCube( - applyRotationalScalar(applyDeadband(controllerOmega.get().getAsDouble()))) - * swerveDrive.getMaximumChassisAngularVelocity(); - speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); - break; - } - case HEADING -> { - omegaRadiansPerSecond = - swerveController.headingCalculate( - swerveDrive.getOdometryHeading().getRadians(), - Rotation2d.fromRadians( - swerveController.getJoystickAngle( - controllerHeadingX.get().getAsDouble(), - controllerHeadingY.get().getAsDouble())) - .getRadians()); - - // Prevent rotation if controller heading inputs are not past axisDeadband - if (Math.abs(controllerHeadingX.get().getAsDouble()) - + Math.abs(controllerHeadingY.get().getAsDouble()) - < axisDeadband.get()) { - omegaRadiansPerSecond = 0; - } - speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); - break; - } - case AIM -> { - Rotation2d currentHeading = swerveDrive.getOdometryHeading(); - Translation2d relativeTrl = - aimTarget.get().relativeTo(swerveDrive.getPose()).getTranslation(); - Rotation2d target = - new Rotation2d(relativeTrl.getX(), relativeTrl.getY()).plus(currentHeading); - omegaRadiansPerSecond = - swerveController.headingCalculate(currentHeading.getRadians(), target.getRadians()); - speeds = new ChassisSpeeds(vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond); - break; - } - case DRIVE_TO_POSE -> { - // Written by team 8865! - ProfiledPIDController translationPIDController = driveToPoseTranslationPIDController.get(); - ProfiledPIDController rotationPIDController = driveToPoseOmegaPIDController.get(); - Pose2d swervePoseSetpoint = driveToPose.get().get(); - Pose2d robotPose = swerveDrive.getPose(); - Vector robotVec = robotPose.getTranslation().toVector(); - Vector targetPoseRelativeToRobotPose = - swervePoseSetpoint.getTranslation().toVector().minus(robotVec); - double distanceFromTarget = targetPoseRelativeToRobotPose.norm(); - - Vector traversalVector = new Vector(Nat.N2()); - traversalVector.set(0, 0, targetPoseRelativeToRobotPose.get(0, 0)); - traversalVector.set(1, 0, targetPoseRelativeToRobotPose.get(1, 0)); - traversalVector = - traversalVector - .unit() - .times(-translationPIDController.calculate(distanceFromTarget, 0)); - - Vector robotForwardVec = - robotPose - .transformBy(new Transform2d(1, 0, new Rotation2d())) - .getTranslation() - .toVector() - .minus(robotVec); - Vector robotLateralVec = - robotPose - .transformBy(new Transform2d(0, 1, new Rotation2d())) - .getTranslation() - .toVector() - .minus(robotVec); - - currentMode = newMode; - speeds = - ChassisSpeeds.fromRobotRelativeSpeeds( - new ChassisSpeeds( - robotForwardVec.norm() * traversalVector.dot(robotForwardVec), - robotLateralVec.norm() * traversalVector.dot(robotLateralVec), - rotationPIDController.calculate( - robotPose.getRotation().getRadians(), - swervePoseSetpoint.getRotation().getRadians())), - swerveDrive.getOdometryHeading()); - double lerpDistance = - robotPose - .getTranslation() - .plus(new Translation2d(speeds.vxMetersPerSecond, vyMetersPerSecond).times(0.02)) - .getDistance(swervePoseSetpoint.getTranslation()); - // Filter out incorrect ChassisSpeeds. - if (lerpDistance > distanceFromTarget) { - speeds = new ChassisSpeeds(0, 0, 0); - } - - return speeds; - } - } - - currentMode = newMode; - - return applyTranslationHeadingOffset(applyRobotRelativeTranslation(speeds)); - } - - /** Drive modes to keep track of. */ - enum SwerveInputMode { - /** Translation only mode, does not allow for rotation and maintains current heading. */ - TRANSLATION_ONLY, - /** Output based off angular velocity */ - ANGULAR_VELOCITY, - /** Output based off of heading. */ - HEADING, - /** Output based off of targeting. */ - AIM, - /** Drive to a target pose. */ - DRIVE_TO_POSE - } -} diff --git a/src/main/java/swervelib/SwerveModule.java b/src/main/java/swervelib/SwerveModule.java deleted file mode 100644 index de367512..00000000 --- a/src/main/java/swervelib/SwerveModule.java +++ /dev/null @@ -1,890 +0,0 @@ -package swervelib; - -import static edu.wpi.first.units.Units.InchesPerSecond; -import static edu.wpi.first.units.Units.MetersPerSecond; -import static edu.wpi.first.units.Units.RadiansPerSecond; -import static edu.wpi.first.units.Units.RotationsPerSecond; - -import edu.wpi.first.math.controller.SimpleMotorFeedforward; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.networktables.BooleanPublisher; -import edu.wpi.first.networktables.DoublePublisher; -import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.units.measure.LinearVelocity; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import swervelib.encoders.SparkMaxEncoderSwerve; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.math.SwerveMath; -import swervelib.motors.SparkMaxBrushedMotorSwerve; -import swervelib.motors.SparkMaxSwerve; -import swervelib.motors.SwerveMotor; -import swervelib.parser.Cache; -import swervelib.parser.PIDFConfig; -import swervelib.parser.SwerveModuleConfiguration; -import swervelib.parser.SwerveModulePhysicalCharacteristics; -import swervelib.simulation.SwerveModuleSimulation; -import swervelib.telemetry.SwerveDriveTelemetry; -import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; - -/** The Swerve Module class which represents and controls Swerve Modules for the swerve drive. */ -public class SwerveModule implements AutoCloseable { - - /** Swerve module configuration options. */ - public final SwerveModuleConfiguration configuration; - /** Absolute encoder position cache. */ - public final Cache absolutePositionCache; - /** Drive motor position cache. */ - public final Cache drivePositionCache; - /** Drive motor velocity cache. */ - public final Cache driveVelocityCache; - /** - * Module number for kinematics, usually 0 to 3. front left -> front right -> back left -> back - * right. - */ - public final int moduleNumber; - /** Swerve Motors. */ - private final SwerveMotor angleMotor, driveMotor; - /** Absolute encoder for swerve drive. */ - private final SwerveAbsoluteEncoder absoluteEncoder; - /** An {@link Alert} for if pushing the Absolute Encoder offset to the encoder fails. */ - private final Alert encoderOffsetWarning; - /** An {@link Alert} for if there is no Absolute Encoder on the module. */ - private final Alert noEncoderWarning; - /** An {@link Alert} for if there is no Absolute Encoder on the module. */ - private final Alert externalSensorIsNull = - new Alert("No absolute Encoder found.", AlertType.kError); - /** An {@link Alert} for if the offset is 0 degrees. */ - private final Alert internalOffsetIsZero = - new Alert("Absolute encoder offset is 0, this may be a problem.", AlertType.kWarning); - /** - * An {@link Alert} for if the angle/steer/azimuth motor is incompatible with the absolute - * encoder. - */ - private final Alert externalFeedbackIncompatible = - new Alert( - "Absolute encoder is incompatible, cannot set as an external feedback device.", - AlertType.kError); - /** An {@link Alert} for if the absolute encoder cannot set an offset. */ - private final Alert externalOffsetIncompatible = - new Alert( - "Absolute encoder is incompatible, cannot set an offset internally.", AlertType.kError); - /** NT4 Raw Absolute Angle publisher for the absolute encoder. */ - private final DoublePublisher rawAbsoluteAnglePublisher; - /** NT4 Adjusted Absolute angle publisher for the absolute encoder. */ - private final DoublePublisher adjAbsoluteAnglePublisher; - /** NT4 Absolute encoder read issue. */ - private final BooleanPublisher absoluteEncoderIssuePublisher; - /** NT4 raw angle motor. */ - private final DoublePublisher rawAnglePublisher; - /** NT4 Raw drive motor. */ - private final DoublePublisher rawDriveEncoderPublisher; - /** NT4 Raw drive motor. */ - private final DoublePublisher rawDriveVelocityPublisher; - /** Speed setpoint publisher for the module motor-controller PID. */ - private final DoublePublisher speedSetpointPublisher; - /** Angle setpoint publisher for the module motor-controller PID. */ - private final DoublePublisher angleSetpointPublisher; - /** Maximum {@link LinearVelocity} for the drive motor of the swerve module. */ - private LinearVelocity maxDriveVelocity; - /** Maximum velocity for the drive motor of the swerve module. */ - private double maxDriveVelocityMetersPerSecond; - /** Maximum {@link AngularVelocity} for the azimuth/angle motor of the swerve module. */ - private AngularVelocity maxAngularVelocity; - /** Feedforward for the drive motor during closed loop control. */ - private SimpleMotorFeedforward driveMotorFeedforward; - /** Anti-Jitter AKA auto-centering disabled. */ - private boolean antiJitterEnabled = true; - /** Last swerve module state applied. */ - private SwerveModuleState lastState; - /** Angle offset from the absolute encoder. */ - private double angleOffset; - /** Simulated swerve module. */ - private SwerveModuleSimulation simModule; - /** Enables utilization off {@link SwerveModuleState#optimize(Rotation2d)} */ - private boolean optimizeSwerveModuleState = true; - /** Encoder synchronization queued. */ - private boolean synchronizeEncoderQueued = false; - /** Encoder, Absolute encoder synchronization enabled. */ - private boolean synchronizeEncoderEnabled = false; - /** Encoder synchronization deadband in degrees. */ - private double synchronizeEncoderDeadband = 3; - - /** - * Construct the swerve module and initialize the swerve module motors and absolute encoder. - * - * @param moduleNumber Module number for kinematics. - * @param moduleConfiguration Module constants containing CAN ID's and offsets. - */ - public SwerveModule(int moduleNumber, SwerveModuleConfiguration moduleConfiguration) { - // angle = 0; - // speed = 0; - // omega = 0; - // fakePos = 0; - this.moduleNumber = moduleNumber; - configuration = moduleConfiguration; - angleOffset = moduleConfiguration.angleOffset; - - // Create motors from configuration and reset them to defaults. - angleMotor = moduleConfiguration.angleMotor; - driveMotor = moduleConfiguration.driveMotor; - angleMotor.factoryDefaults(); - driveMotor.factoryDefaults(); - - // Initialize Feedforwards. - driveMotorFeedforward = getDefaultFeedforward(); - - // Configure voltage comp, current limit, and ramp rate. - angleMotor.setVoltageCompensation(configuration.physicalCharacteristics.optimalVoltage); - driveMotor.setVoltageCompensation(configuration.physicalCharacteristics.optimalVoltage); - angleMotor.setCurrentLimit(configuration.physicalCharacteristics.angleMotorCurrentLimit); - driveMotor.setCurrentLimit(configuration.physicalCharacteristics.driveMotorCurrentLimit); - angleMotor.setLoopRampRate(configuration.physicalCharacteristics.angleMotorRampRate); - driveMotor.setLoopRampRate(configuration.physicalCharacteristics.driveMotorRampRate); - - // Config angle encoders - absoluteEncoder = moduleConfiguration.absoluteEncoder; - if (absoluteEncoder != null) { - absoluteEncoder.factoryDefault(); - absoluteEncoder.configure(moduleConfiguration.absoluteEncoderInverted); - } - - if (SwerveDriveTelemetry.isSimulation) { - simModule = new SwerveModuleSimulation(); - } - - // Setup the cache for the absolute encoder position. - absolutePositionCache = new Cache<>(this::getRawAbsolutePosition, 20); - - // Config angle motor/controller - if (!angleMotor.usingExternalFeedbackSensor()) { - angleMotor.configureIntegratedEncoder(moduleConfiguration.conversionFactors.angle.factor); - } - angleMotor.configurePIDF(moduleConfiguration.anglePIDF); - angleMotor.configurePIDWrapping(0, 360); - angleMotor.setInverted(moduleConfiguration.angleMotorInverted); - angleMotor.setMotorBrake(false); - - // Set the position AFTER settings the conversion factor. - if (absoluteEncoder != null) { - angleMotor.setPosition(getAbsolutePosition()); - } - - // Config drive motor/controller - driveMotor.configureIntegratedEncoder(moduleConfiguration.conversionFactors.drive.factor); - driveMotor.configurePIDF(moduleConfiguration.velocityPIDF); - driveMotor.setInverted(moduleConfiguration.driveMotorInverted); - driveMotor.setMotorBrake(true); - - driveMotor.burnFlash(); - angleMotor.burnFlash(); - - drivePositionCache = new Cache<>(driveMotor::getPosition, 20); - driveVelocityCache = new Cache<>(driveMotor::getVelocity, 20); - - // Force a cache update on init. - driveVelocityCache.update(); - drivePositionCache.update(); - absolutePositionCache.update(); - - // Save the current state. - lastState = getState(); - - noEncoderWarning = - new Alert( - "Motors", - "There is no Absolute Encoder on module #" + moduleNumber, - AlertType.kWarning); - encoderOffsetWarning = - new Alert( - "Motors", - "Pushing the Absolute Encoder offset to the encoder failed on module #" + moduleNumber, - AlertType.kWarning); - - rawAbsoluteAnglePublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Absolute Encoder") - .publish(); - adjAbsoluteAnglePublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Adjusted Absolute Encoder") - .publish(); - absoluteEncoderIssuePublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getBooleanTopic( - "swerve/modules/" + configuration.name + "/Absolute Encoder Read Issue") - .publish(); - rawAnglePublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Angle Encoder") - .publish(); - rawDriveEncoderPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Drive Encoder") - .publish(); - rawDriveVelocityPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Raw Drive Velocity") - .publish(); - speedSetpointPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Speed Setpoint") - .publish(); - angleSetpointPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/modules/" + configuration.name + "/Angle Setpoint") - .publish(); - } - - @Override - public void close() { - angleMotor.close(); - driveMotor.close(); - absoluteEncoder.close(); - } - - /** - * Get the default {@link SimpleMotorFeedforward} for the swerve module drive motor. - * - * @return {@link SimpleMotorFeedforward} using motor details. - */ - public SimpleMotorFeedforward getDefaultFeedforward() { - double nominalVoltage = driveMotor.getSimMotor().nominalVoltageVolts; - double maxDriveSpeedMPS = getMaxVelocity().in(MetersPerSecond); - return SwerveMath.createDriveFeedforward( - nominalVoltage, - maxDriveSpeedMPS, - configuration.physicalCharacteristics.wheelGripCoefficientOfFriction); - } - - /** - * Set utilization of {@link SwerveModuleState#optimize(Rotation2d)} which should be disabled for - * some debugging. - * - * @param optimizationState Optimization enabled. - */ - public void setModuleStateOptimization(boolean optimizationState) { - optimizeSwerveModuleState = optimizationState; - if (!optimizeSwerveModuleState) { - angleMotor.disablePIDWrapping(); - angleMotor.burnFlash(); - } - } - - /** - * Check if the module state optimization used by {@link SwerveModuleState#optimize(Rotation2d)} - * is enabled. - * - * @return optimization state. - */ - public boolean getModuleStateOptimization() { - return optimizeSwerveModuleState; - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param optimalVoltage Nominal voltage for operation to output to. - */ - public void setAngleMotorVoltageCompensation(double optimalVoltage) { - angleMotor.setVoltageCompensation(optimalVoltage); - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param optimalVoltage Nominal voltage for operation to output to. - */ - public void setDriveMotorVoltageCompensation(double optimalVoltage) { - driveMotor.setVoltageCompensation(optimalVoltage); - } - - /** Queue synchronization of the integrated angle encoder with the absolute encoder. */ - public void queueSynchronizeEncoders() { - if (absoluteEncoder != null && synchronizeEncoderEnabled) { - synchronizeEncoderQueued = true; - } - } - - /** - * Enable auto synchronization for encoders during a match. This will only occur when the modules - * are not moving for a few seconds. - * - * @param enabled Enable state - * @param deadband Deadband in degrees, default is 3 degrees. - */ - public void setEncoderAutoSynchronize(boolean enabled, double deadband) { - synchronizeEncoderEnabled = enabled; - synchronizeEncoderDeadband = deadband; - } - - /** - * Enable auto synchronization for encoders during a match. This will only occur when the modules - * are not moving for a few seconds. - * - * @param enabled Enable state - */ - public void setEncoderAutoSynchronize(boolean enabled) { - synchronizeEncoderEnabled = enabled; - } - - /** - * Set the antiJitter functionality, if true the modules will NOT auto center. Pushes the offsets - * to the angle motor controllers as well. - * - * @param antiJitter Anti-Jitter state desired. - */ - public void setAntiJitter(boolean antiJitter) { - this.antiJitterEnabled = antiJitter; - if (antiJitter) { - pushOffsetsToEncoders(); - } else { - restoreInternalOffset(); - } - } - - /** - * Set the feedforward attributes to the given parameters. - * - * @param drive Drive motor feedforward for the module. - */ - public void setFeedforward(SimpleMotorFeedforward drive) { - this.driveMotorFeedforward = drive; - } - - /** - * Get the current drive motor PIDF values. - * - * @return {@link PIDFConfig} of the drive motor. - */ - public PIDFConfig getDrivePIDF() { - return configuration.velocityPIDF; - } - - /** - * Set the drive PIDF values. - * - * @param config {@link PIDFConfig} of that should be set. - */ - public void setDrivePIDF(PIDFConfig config) { - configuration.velocityPIDF = config; - driveMotor.configurePIDF(config); - } - - /** - * Get the current angle/azimuth/steering motor PIDF values. - * - * @return {@link PIDFConfig} of the angle motor. - */ - public PIDFConfig getAnglePIDF() { - return configuration.anglePIDF; - } - - /** - * Set the angle/azimuth/steering motor PID - * - * @param config {@link PIDFConfig} of that should be set. - */ - public void setAnglePIDF(PIDFConfig config) { - configuration.anglePIDF = config; - angleMotor.configurePIDF(config); - } - - /** - * Set the desired state of the swerve module.
- * WARNING: If you are not using one of the functions from {@link SwerveDrive} you may screw up - * {@link SwerveDrive#kinematics} - * - * @param desiredState Desired swerve module state. - * @param isOpenLoop Whether to use open loop (direct percent) or direct velocity control. - * @param force Disables optimizations that prevent movement in the angle motor and forces the - * desired state onto the swerve module. - */ - public void setDesiredState(SwerveModuleState desiredState, boolean isOpenLoop, boolean force) { - applyStateOptimizations(desiredState); - applyAntiJitter(desiredState, force); - - // Cosine compensation. - double nextVelocityMetersPerSecond = - configuration.useCosineCompensator - ? getCosineCompensatedVelocity(desiredState) - : desiredState.speedMetersPerSecond; - double curVelocityMetersPerSecond = lastState.speedMetersPerSecond; - desiredState.speedMetersPerSecond = nextVelocityMetersPerSecond; - - setDesiredState( - desiredState, - isOpenLoop, - driveMotorFeedforward.calculateWithVelocities( - curVelocityMetersPerSecond, nextVelocityMetersPerSecond)); - } - - /** - * Set the desired state of the swerve module.
- * WARNING: If you are not using one of the functions from {@link SwerveDrive} you may screw up - * {@link SwerveDrive#kinematics} - * - * @param desiredState Desired swerve module state. - * @param isOpenLoop Whether to use open loop (direct percent) or direct velocity control. - * @param driveFeedforwardVoltage Drive motor controller feedforward as a voltage. - */ - public void setDesiredState( - SwerveModuleState desiredState, boolean isOpenLoop, double driveFeedforwardVoltage) { - if (isOpenLoop) { - double percentOutput = - desiredState.speedMetersPerSecond / maxDriveVelocity.in(MetersPerSecond); - driveMotor.setVoltage(percentOutput * 12); - } else { - driveMotor.setReference(desiredState.speedMetersPerSecond, driveFeedforwardVoltage); - } - - // Prevent module rotation if angle is the same as the previous angle. - // Synchronize encoders if queued and send in the current position as the value from the - // absolute encoder. - if (absoluteEncoder != null && synchronizeEncoderQueued && synchronizeEncoderEnabled) { - double absoluteEncoderPosition = getAbsolutePosition(); - if (Math.abs(angleMotor.getPosition() - absoluteEncoderPosition) - >= synchronizeEncoderDeadband) { - angleMotor.setPosition(absoluteEncoderPosition); - } - angleMotor.setReference(desiredState.angle.getDegrees(), 0, absoluteEncoderPosition); - synchronizeEncoderQueued = false; - } else { - angleMotor.setReference(desiredState.angle.getDegrees(), 0); - } - - lastState = desiredState; - - if (SwerveDriveTelemetry.isSimulation) { - simModule.updateStateAndPosition(desiredState); - } - - // TODO: Change and move to SwerveDriveTelemetry - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.INFO.ordinal()) { - SwerveDriveTelemetry.desiredStatesObj[moduleNumber] = desiredState; - } - - if (SwerveDriveTelemetry.verbosity == TelemetryVerbosity.HIGH) { - speedSetpointPublisher.set(desiredState.speedMetersPerSecond); - angleSetpointPublisher.set(desiredState.angle.getDegrees()); - } - - if (moduleNumber == SwerveDriveTelemetry.moduleCount - 1) { - SwerveDriveTelemetry.endCtrlCycle(); - } - } - - /** - * Get the cosine compensated velocity to set the swerve module to. - * - * @param desiredState Desired {@link SwerveModuleState} to use. - * @return Cosine compensated velocity in meters/second. - */ - private double getCosineCompensatedVelocity(SwerveModuleState desiredState) { - double cosineScalar = 1.0; - // Taken from the CTRE SwerveModule class. - // https://api.ctr-electronics.com/phoenix6/release/java/src-html/com/ctre/phoenix6/mechanisms/swerve/SwerveModule.html#line.46 - /* From FRC 900's whitepaper, we add a cosine compensator to the applied drive velocity */ - /* To reduce the "skew" that occurs when changing direction */ - /* If error is close to 0 rotations, we're already there, so apply full power */ - /* If the error is close to 0.25 rotations, then we're 90 degrees, so movement doesn't help us at all */ - cosineScalar = - Rotation2d.fromDegrees(desiredState.angle.getDegrees()) - .minus(Rotation2d.fromDegrees(getAbsolutePosition())) - .getCos(); // TODO: Investigate angle modulus by 180. - /* Make sure we don't invert our drive, even though we shouldn't ever target over 90 degrees anyway */ - if (cosineScalar < 0.0) { - cosineScalar = 1; - } - - return desiredState.speedMetersPerSecond * cosineScalar; - } - - /** - * Apply the {@link SwerveModuleState#optimize(Rotation2d)} function if the module state - * optimization is enabled while debugging. - * - * @param desiredState The desired state to apply the optimization to. - */ - public void applyStateOptimizations(SwerveModuleState desiredState) { - // SwerveModuleState optimization might be desired to be disabled while debugging. - if (optimizeSwerveModuleState) { - desiredState.optimize(Rotation2d.fromDegrees(getAbsolutePosition())); - } - } - - /** - * Apply anti-jitter to the desired state. This will prevent the module from rotating if the speed - * requested is too low. If force is true, the anti-jitter will not be applied. - * - * @param desiredState The desired state to apply the anti-jitter to. - * @param force Whether to ignore the {@link SwerveModule#antiJitterEnabled} state and apply the - * anti-jitter anyway. - */ - public void applyAntiJitter(SwerveModuleState desiredState, boolean force) { - if (!force && antiJitterEnabled) { - // Prevents module rotation if speed is less than 1% - SwerveMath.antiJitter(desiredState, lastState, Math.min(maxDriveVelocityMetersPerSecond, 4)); - } - } - - /** - * Set the angle for the module. - * - * @param angle Angle in degrees. - */ - public void setAngle(double angle) { - angleMotor.setReference(angle, 0); - lastState.angle = Rotation2d.fromDegrees(angle); - } - - /** - * Get the Swerve Module state. - * - * @return Current SwerveModule state. - */ - public SwerveModuleState getState() { - double velocity; - Rotation2d azimuth; - if (!SwerveDriveTelemetry.isSimulation) { - velocity = driveVelocityCache.getValue(); - azimuth = Rotation2d.fromDegrees(getAbsolutePosition()); - } else { - return simModule.getState(); - } - return new SwerveModuleState(velocity, azimuth); - } - - /** - * Get the position of the swerve module. - * - * @return {@link SwerveModulePosition} of the swerve module. - */ - public SwerveModulePosition getPosition() { - double position; - Rotation2d azimuth; - if (!SwerveDriveTelemetry.isSimulation) { - position = drivePositionCache.getValue(); - azimuth = Rotation2d.fromDegrees(getAbsolutePosition()); - } else { - return simModule.getPosition(); - } - return new SwerveModulePosition(position, azimuth); - } - - /** - * Get the absolute position. Falls back to relative position on reading failure. - * - * @return Absolute encoder angle in degrees in the range [0, 360). - */ - public double getAbsolutePosition() { - return absolutePositionCache.getValue(); - } - - /** - * Get the absolute position. Falls back to relative position on reading failure. - * - * @return Absolute encoder angle in degrees in the range [0, 360). - */ - public double getRawAbsolutePosition() { - /* During simulation, when no absolute encoders are available, we return the state from the simulation module instead. */ - if (SwerveDriveTelemetry.isSimulation) { - Rotation2d absolutePosition = simModule.getState().angle; - return absolutePosition.getDegrees(); - } - - double angle; - if (absoluteEncoder != null) { - angle = absoluteEncoder.getAbsolutePosition() - angleOffset; - if (absoluteEncoder.readingError) { - angle = getRelativePosition(); - } - } else { - angle = getRelativePosition(); - } - if (optimizeSwerveModuleState) { - angle %= 360; - if (angle < 0.0) { - angle += 360; - } - } - - return angle; - } - - /** - * Get the relative angle in degrees. - * - * @return Angle in degrees. - */ - public double getRelativePosition() { - return angleMotor.getPosition(); - } - - /** - * Set the brake mode. - * - * @param brake Set the brake mode. - */ - public void setMotorBrake(boolean brake) { - driveMotor.setMotorBrake(brake); - } - - /** - * Set the conversion factor for the angle/azimuth motor controller. - * - * @param conversionFactor Angle motor conversion factor for PID, should be generated from {@link - * SwerveMath#calculateDegreesPerSteeringRotation(double, double)} or calculated. - */ - public void setAngleMotorConversionFactor(double conversionFactor) { - angleMotor.configureIntegratedEncoder(conversionFactor); - } - - /** - * Set the conversion factor for the drive motor controller. - * - * @param conversionFactor Drive motor conversion factor for PID, should be generated from {@link - * SwerveMath#calculateMetersPerRotation(double, double, double)} or calculated. - */ - public void setDriveMotorConversionFactor(double conversionFactor) { - driveMotor.configureIntegratedEncoder(conversionFactor); - } - - /** - * Get the angle {@link SwerveMotor} for the {@link SwerveModule}. - * - * @return {@link SwerveMotor} for the angle/steering motor of the module. - */ - public SwerveMotor getAngleMotor() { - return angleMotor; - } - - /** - * Get the drive {@link SwerveMotor} for the {@link SwerveModule}. - * - * @return {@link SwerveMotor} for the drive motor of the module. - */ - public SwerveMotor getDriveMotor() { - return driveMotor; - } - - /** - * Get the {@link SwerveAbsoluteEncoder} for the {@link SwerveModule}. - * - * @return {@link SwerveAbsoluteEncoder} for the swerve module. - */ - public SwerveAbsoluteEncoder getAbsoluteEncoder() { - return absoluteEncoder; - } - - /** - * Fetch the {@link SwerveModuleConfiguration} for the {@link SwerveModule} with the parsed - * configurations. - * - * @return {@link SwerveModuleConfiguration} for the {@link SwerveModule}. - */ - public SwerveModuleConfiguration getConfiguration() { - return configuration; - } - - /** Use external sensors for the feedback of the angle/azimuth/steer controller. */ - public void useExternalFeedbackSensor() { - if (absoluteEncoder == null) { - externalSensorIsNull.set(true); - return; - } - if (angleOffset == 0) { - internalOffsetIsZero.set(true); - } - if (absoluteEncoder.setAbsoluteEncoderOffset(configuration.angleOffset)) { - angleMotor.setAbsoluteEncoder(absoluteEncoder); - if (angleMotor.usingExternalFeedbackSensor()) { - angleOffset = 0; - } else { - externalFeedbackIncompatible.set(true); - angleMotor.setAbsoluteEncoder(null); - absoluteEncoder.setAbsoluteEncoderOffset(0); - } - - } else { - externalOffsetIncompatible.set(true); - absoluteEncoder.setAbsoluteEncoderOffset(0); - } - } - - /** Use external sensors for the feedback of the angle/azimuth/steer controller. */ - public void useInternalFeedbackSensor() { - if (absoluteEncoder == null) { - externalSensorIsNull.set(true); - return; - } - if (angleOffset == 0) { - internalOffsetIsZero.set(true); - } - angleMotor.setAbsoluteEncoder(null); - absoluteEncoder.setAbsoluteEncoderOffset(0); - angleOffset = configuration.angleOffset; - } - - /** - * Push absolute encoder offset in the memory of the encoder or controller. Also removes the - * internal angle offset. - */ - @Deprecated - public void pushOffsetsToEncoders() { - if (absoluteEncoder != null && angleOffset == configuration.angleOffset) { - // If the absolute encoder is attached. - if (angleMotor instanceof SparkMaxSwerve - || angleMotor instanceof SparkMaxBrushedMotorSwerve) { - if (absoluteEncoder instanceof SparkMaxEncoderSwerve) { - angleMotor.setAbsoluteEncoder(absoluteEncoder); - if (absoluteEncoder.setAbsoluteEncoderOffset(angleOffset)) { - angleOffset = 0; - } else { - angleMotor.setAbsoluteEncoder(null); - encoderOffsetWarning.set(true); - } - } - } - - } else { - noEncoderWarning.set(true); - } - } - - /** - * Restore internal offset in YAGSL and either sets absolute encoder offset to 0 or restores old - * value. - */ - public void restoreInternalOffset() { - angleMotor.setAbsoluteEncoder(null); - absoluteEncoder.setAbsoluteEncoderOffset(0); - angleOffset = configuration.angleOffset; - } - - /** - * Get if the last Absolute Encoder had a read issue, such as it does not exist. - * - * @return If the last Absolute Encoder had a read issue, or absolute encoder does not exist. - */ - public boolean getAbsoluteEncoderReadIssue() { - if (absoluteEncoder == null) { - return true; - } else { - return absoluteEncoder.readingError; - } - } - - /** - * Get the maximum module velocity as a {@link LinearVelocity} based on the RPM and gear ratio. - * - * @return {@link LinearVelocity} max velocity of the drive wheel. - */ - public LinearVelocity getMaxVelocity() { - getMaxDriveVelocityMetersPerSecond(); - return maxDriveVelocity; - } - - /** - * Get the maximum drive velocity of the module in Meters Per Second. - * - * @return Maximum drive motor velocity in Meters Per Second. - */ - public double getMaxDriveVelocityMetersPerSecond() { - if (maxDriveVelocity == null) { - maxDriveVelocity = - InchesPerSecond.of( - (driveMotor.getSimMotor().freeSpeedRadPerSec - / configuration.conversionFactors.drive.gearRatio) - * configuration.conversionFactors.drive.diameter - / 2.0); - maxDriveVelocityMetersPerSecond = maxDriveVelocity.in(MetersPerSecond); - } - return maxDriveVelocityMetersPerSecond; - } - - /** - * Get the maximum module angular velocity as a {@link AngularVelocity} based on the RPM and gear - * ratio. - * - * @return {@link AngularVelocity} max velocity of the angle/azimuth. - */ - public AngularVelocity getMaxAngularVelocity() { - if (maxAngularVelocity == null) { - maxAngularVelocity = - RotationsPerSecond.of( - RadiansPerSecond.of(angleMotor.getSimMotor().freeSpeedRadPerSec) - .in(RotationsPerSecond) - / configuration.conversionFactors.angle.gearRatio); - } - return maxAngularVelocity; - } - - /** Update data sent to {@link SmartDashboard}. */ - public void updateTelemetry() { - if (absoluteEncoder != null) { - rawAbsoluteAnglePublisher.set(absoluteEncoder.getAbsolutePosition()); - } - if (SwerveDriveTelemetry.isSimulation - && SwerveDriveTelemetry.verbosity == TelemetryVerbosity.HIGH) { - SwerveModulePosition pos = simModule.getPosition(); - SwerveModuleState state = simModule.getState(); - rawAnglePublisher.set(pos.angle.getDegrees()); - rawDriveEncoderPublisher.set(pos.distanceMeters); - rawDriveVelocityPublisher.set(state.speedMetersPerSecond); - // For code coverage - angleMotor.getPosition(); - drivePositionCache.getValue(); - driveVelocityCache.getValue(); - } else { - rawAnglePublisher.set(angleMotor.getPosition()); - rawDriveEncoderPublisher.set(drivePositionCache.getValue()); - rawDriveVelocityPublisher.set(driveVelocityCache.getValue()); - } - adjAbsoluteAnglePublisher.set(getAbsolutePosition()); - absoluteEncoderIssuePublisher.set(getAbsoluteEncoderReadIssue()); - } - - /** Invalidate the {@link Cache} objects used by {@link SwerveModule}. */ - public void invalidateCache() { - absolutePositionCache.update(); - drivePositionCache.update(); - driveVelocityCache.update(); - } - - /** - * Obtains the {@link SwerveModuleSimulation} used in simulation. - * - * @return the module simulation, null if this method is called on a real robot - */ - public SwerveModuleSimulation getSimModule() { - return simModule; - } - - /** - * Configure the {@link SwerveModule#simModule} with the MapleSim {@link - * org.ironmaple.simulation.drivesims.SwerveModuleSimulation} - * - * @param swerveModuleSimulation MapleSim {@link - * org.ironmaple.simulation.drivesims.SwerveModuleSimulation} to configure with. - * @param physicalCharacteristics {@link SwerveModulePhysicalCharacteristics} that represent the - * swerve drive. - */ - public void configureModuleSimulation( - org.ironmaple.simulation.drivesims.SwerveModuleSimulation swerveModuleSimulation, - SwerveModulePhysicalCharacteristics physicalCharacteristics) { - this.simModule.configureSimModule(swerveModuleSimulation, physicalCharacteristics); - } -} diff --git a/src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java b/src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java deleted file mode 100644 index af58444c..00000000 --- a/src/main/java/swervelib/encoders/AnalogAbsoluteEncoderSwerve.java +++ /dev/null @@ -1,126 +0,0 @@ -package swervelib.encoders; - -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.AnalogInput; -import edu.wpi.first.wpilibj.RobotController; - -/** Swerve Absolute Encoder for Thrifty Encoders and other analog encoders. */ -public class AnalogAbsoluteEncoderSwerve extends SwerveAbsoluteEncoder { - // Entire class inspired by 5010 - // Source: - // https://github.com/FRC5010/FRCLibrary/blob/main/FRC5010Example2023/src/main/java/frc/robot/FRC5010/sensors/AnalogInput5010.java - /** Encoder as Analog Input. */ - public AnalogInput encoder; - /** Inversion state of the encoder. */ - private boolean inverted = false; - /** An {@link Alert} for if the absolute encoder offset cannot be set. */ - private Alert cannotSetOffset; - /** - * An {@link Alert} detailing how the analog absolute encoder may not report accurate velocities. - */ - private Alert inaccurateVelocities; - - /** - * Construct the Thrifty Encoder as a Swerve Absolute Encoder. - * - * @param encoder Encoder to construct. - */ - public AnalogAbsoluteEncoderSwerve(AnalogInput encoder) { - this.encoder = encoder; - cannotSetOffset = - new Alert( - "Encoders", - "Cannot Set Absolute Encoder Offset of Analog Encoders Channel #" - + encoder.getChannel(), - AlertType.kWarning); - inaccurateVelocities = - new Alert( - "Encoders", - "The Analog Absolute encoder may not report accurate velocities!", - AlertType.kWarning); - } - - @Override - public void close() { - encoder.close(); - } - - /** - * Construct the Encoder given the analog input channel. - * - * @param channel Analog Input channel of which the encoder resides. - */ - public AnalogAbsoluteEncoderSwerve(int channel) { - this(new AnalogInput(channel)); - } - - /** Reset the encoder to factory defaults. */ - @Override - public void factoryDefault() { - // Do nothing - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - // Do nothing - } - - /** - * Configure the absolute encoder to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - this.inverted = inverted; - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - return (inverted ? -1.0 : 1.0) - * (encoder.getAverageVoltage() / RobotController.getVoltage5V()) - * 360; - } - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Cannot Set the offset of an Analog Absolute Encoder. - * - * @param offset the offset the Absolute Encoder uses as the zero point. - * @return Will always be false as setting the offset is unsupported of an Analog absolute - * encoder. - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - // Do Nothing - cannotSetOffset.set(true); - return false; - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - inaccurateVelocities.set(true); - return encoder.getValue() * 360; - } -} diff --git a/src/main/java/swervelib/encoders/CANCoderSwerve.java b/src/main/java/swervelib/encoders/CANCoderSwerve.java deleted file mode 100644 index 906c0c7f..00000000 --- a/src/main/java/swervelib/encoders/CANCoderSwerve.java +++ /dev/null @@ -1,211 +0,0 @@ -package swervelib.encoders; - -import static edu.wpi.first.units.Units.DegreesPerSecond; -import static edu.wpi.first.units.Units.Milliseconds; -import static edu.wpi.first.units.Units.Rotations; -import static edu.wpi.first.units.Units.Seconds; - -import com.ctre.phoenix6.StatusCode; -import com.ctre.phoenix6.StatusSignal; -import com.ctre.phoenix6.configs.CANcoderConfiguration; -import com.ctre.phoenix6.configs.CANcoderConfigurator; -import com.ctre.phoenix6.hardware.CANcoder; -import com.ctre.phoenix6.signals.MagnetHealthValue; -import com.ctre.phoenix6.signals.SensorDirectionValue; -import edu.wpi.first.units.measure.Angle; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; - -/** Swerve Absolute Encoder for CTRE CANCoders. */ -public class CANCoderSwerve extends SwerveAbsoluteEncoder { - - /** Wait time for status frames to show up. */ - public static double STATUS_TIMEOUT_SECONDS = Milliseconds.of(1).in(Seconds); - /** An {@link Alert} for if the CANCoder magnet field is less than ideal. */ - private final Alert magnetFieldLessThanIdeal; - /** An {@link Alert} for if the CANCoder reading is faulty. */ - private final Alert readingFaulty; - /** An {@link Alert} for if the CANCoder reading is faulty and the reading is ignored. */ - private final Alert readingIgnored; - /** An {@link Alert} for if the absolute encoder offset cannot be set. */ - private final Alert cannotSetOffset; - /** Magnet Health status signal for the CANCoder. */ - private final StatusSignal magnetHealth; - /** CANCoder reading cache. */ - private final StatusSignal angle; - /** Angular velocity of the {@link CANcoder}. */ - private final StatusSignal velocity; - /** CANCoder with WPILib sendable and support. */ - public CANcoder encoder; - /** {@link CANcoder} Configurator objet for this class. */ - private CANcoderConfigurator config; - /** {@link CANcoderConfiguration} object for the CANcoder. */ - private CANcoderConfiguration cfg = new CANcoderConfiguration(); - - /** - * Initialize the CANCoder on the standard CANBus. - * - * @param id CAN ID. - */ - public CANCoderSwerve(int id) { - // Empty string uses the default canbus for the system - this(id, ""); - } - - /** - * Initialize the CANCoder on the CANivore. - * - * @param id CAN ID of the {@link CANcoder}. - * @param canbus CAN bus to initialize it on. Should be "rio" or "" if the RIO CANbus, else is the - * CANivore name. - */ - public CANCoderSwerve(int id, String canbus) { - encoder = new CANcoder(id, canbus); - config = encoder.getConfigurator(); - magnetHealth = encoder.getMagnetHealth(); - angle = encoder.getAbsolutePosition(); - velocity = encoder.getVelocity(); - magnetFieldLessThanIdeal = - new Alert( - "Encoders", - "CANCoder " + encoder.getDeviceID() + " magnetic field is less than ideal.", - AlertType.kWarning); - readingFaulty = - new Alert( - "Encoders", - "CANCoder " + encoder.getDeviceID() + " reading was faulty.", - AlertType.kWarning); - readingIgnored = - new Alert( - "Encoders", - "CANCoder " + encoder.getDeviceID() + " reading was faulty, ignoring.", - AlertType.kWarning); - cannotSetOffset = - new Alert( - "Encoders", - "Failure to set CANCoder " + encoder.getDeviceID() + " Absolute Encoder Offset", - AlertType.kWarning); - } - - @Override - public void close() { - encoder.close(); - } - - /** Reset the encoder to factory defaults. */ - @Override - public void factoryDefault() { - cfg = new CANcoderConfiguration(); - config.apply(cfg); - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - encoder.clearStickyFaults(); - } - - /** - * Configure the absolute encoder to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - config.refresh(cfg.MagnetSensor); - config.apply( - cfg.MagnetSensor.withAbsoluteSensorDiscontinuityPoint(Rotations.of(1)) - .withSensorDirection( - inverted - ? SensorDirectionValue.Clockwise_Positive - : SensorDirectionValue.CounterClockwise_Positive)); - } - - /** - * Get the absolute position of the encoder. Sets {@link SwerveAbsoluteEncoder#readingError} on - * erroneous readings. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - readingError = false; - MagnetHealthValue strength = magnetHealth.refresh().getValue(); - angle.refresh(); - - magnetFieldLessThanIdeal.set(strength != MagnetHealthValue.Magnet_Green); - if (strength == MagnetHealthValue.Magnet_Invalid || strength == MagnetHealthValue.Magnet_Red) { - readingError = true; - readingFaulty.set(true); - return 0; - } else { - readingFaulty.set(false); - } - - // Taken from democat's library. - // Source: - // https://github.com/democat3457/swerve-lib/blob/7c03126b8c22f23a501b2c2742f9d173a5bcbc40/src/main/java/com/swervedrivespecialties/swervelib/ctre/CanCoderFactoryBuilder.java#L51-L74 - for (int i = 0; i < maximumRetries; i++) { - if (angle.getStatus() == StatusCode.OK) { - break; - } - angle.waitForUpdate(STATUS_TIMEOUT_SECONDS); - } - if (angle.getStatus() != StatusCode.OK) { - readingError = true; - readingIgnored.set(true); - } else { - readingIgnored.set(false); - } - // Convert from Rotations to Degrees. - return angle.getValueAsDouble() * 360; - } - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Sets the Absolute Encoder Offset within the CANcoder's Memory. - * - * @param offset the offset the Absolute Encoder uses as the zero point in degrees. - * @return if setting Absolute Encoder Offset was successful or not. - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - StatusCode error = config.refresh(cfg.MagnetSensor); - if (error != StatusCode.OK) { - return false; - } - - error = config.apply(cfg.MagnetSensor.withMagnetOffset(offset / 360)); - cannotSetOffset.setText( - "Failure to set CANCoder " - + encoder.getDeviceID() - + " Absolute Encoder Offset Error: " - + error); - if (error == StatusCode.OK) { - cannotSetOffset.set(false); - return true; - } - cannotSetOffset.set(true); - return false; - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - return velocity.refresh().getValue().in(DegreesPerSecond); - } -} diff --git a/src/main/java/swervelib/encoders/CanAndMagSwerve.java b/src/main/java/swervelib/encoders/CanAndMagSwerve.java deleted file mode 100644 index f8bff1d9..00000000 --- a/src/main/java/swervelib/encoders/CanAndMagSwerve.java +++ /dev/null @@ -1,97 +0,0 @@ -package swervelib.encoders; - -import com.reduxrobotics.sensors.canandmag.Canandmag; -import com.reduxrobotics.sensors.canandmag.CanandmagSettings; - -/** HELIUM {@link Canandmag} from ReduxRobotics absolute encoder, attached through the CAN bus. */ -public class CanAndMagSwerve extends SwerveAbsoluteEncoder { - - /** The {@link Canandmag} representing the CANandMag on the CAN bus. */ - public Canandmag encoder; - /** The {@link Canandmag} settings object to use. */ - public CanandmagSettings settings; - - /** - * Create the {@link Canandmag} - * - * @param canid The CAN ID whenever the CANandMag is operating on the CANBus. - */ - public CanAndMagSwerve(int canid) { - encoder = new Canandmag(canid); - settings = new CanandmagSettings(); - } - - @Override - public void close() { - encoder.close(); - } - - /** - * Reset the encoder to factory defaults. - * - *

This will not clear the stored zero offset. - */ - @Override - public void factoryDefault() { - encoder.resetFactoryDefaults(false); - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - encoder.clearStickyFaults(); - } - - /** - * Configure the CANandMag to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - settings.setInvertDirection(inverted); - encoder.setSettings(settings); - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - return encoder.getAbsPosition() * 360; - } - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Cannot set the offset of the CANandMag. - * - * @param offset the offset the Absolute Encoder uses as the zero point. - * @return true if setting the zero point succeeded, false otherwise - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - settings.setZeroOffset(offset); - return encoder.setSettings(settings); - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - return encoder.getVelocity() * 360; - } -} diff --git a/src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java b/src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java deleted file mode 100644 index 112ba3a8..00000000 --- a/src/main/java/swervelib/encoders/DIODutyCycleEncoderSwerve.java +++ /dev/null @@ -1,106 +0,0 @@ -package swervelib.encoders; - -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.DutyCycleEncoder; -import edu.wpi.first.wpilibj.Timer; - -/** - * DutyCycle encoders such as "US Digital MA3 with DIO Output, the CTRE Mag Encoder, the Rev Hex - * Encoder, and the AM Mag Encoder." attached via a DIO lane. - * - *

A more formal definition: returns the closest angle {@code n} to {@code scopeReference} such - * that {@code n} is congruent to {@code newAngle}. - * - * @param scopeReference Current Angle (deg) - * @param newAngle Target Angle (deg) - * @return Closest angle within scope (deg) - */ - public static double placeInAppropriate0To360Scope(double scopeReference, double newAngle) { - // Figure out how many revolutions from the angle to the reference - double diffRevs = Math.round((scopeReference - newAngle) / 360) * 360; - - // Add that many revolutions - return diffRevs + newAngle; - } - - /** - * Perform anti-jitter within modules if the speed requested is too low. - * - * @param moduleState Current {@link SwerveModuleState} requested. - * @param lastModuleState Previous {@link SwerveModuleState} used. - * @param maxSpeed Maximum speed of the modules. - */ - public static void antiJitter( - SwerveModuleState moduleState, SwerveModuleState lastModuleState, double maxSpeed) { - if (Math.abs(moduleState.speedMetersPerSecond) <= (maxSpeed * 0.01)) { - moduleState.angle = lastModuleState.angle; - } - } - - /** - * Cube the {@link Translation2d} magnitude given in Polar coordinates. - * - * @param translation {@link Translation2d} to manipulate. - * @return Cubed magnitude from {@link Translation2d}. - */ - public static Translation2d cubeTranslation(Translation2d translation) { - if (Math.hypot(translation.getX(), translation.getY()) <= 1.0E-6) { - return translation; - } - return new Translation2d(Math.pow(translation.getNorm(), 3), translation.getAngle()); - } - - /** - * Scale the {@link Translation2d} Polar coordinate magnitude. - * - * @param translation {@link Translation2d} to use. - * @param scalar Multiplier for the Polar coordinate magnitude to use. - * @return {@link Translation2d} scaled by given magnitude scalar. - */ - public static Translation2d scaleTranslation(Translation2d translation, double scalar) { - if (Math.hypot(translation.getX(), translation.getY()) <= 1.0E-6) { - return translation; - } - return new Translation2d(translation.getNorm() * scalar, translation.getAngle()); - } -} diff --git a/src/main/java/swervelib/math/package-info.java b/src/main/java/swervelib/math/package-info.java deleted file mode 100644 index 633b280c..00000000 --- a/src/main/java/swervelib/math/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Mathematics for swerve drives. Original second order kinematics was developed by Team 3181 here. - * - */ -package swervelib.math; diff --git a/src/main/java/swervelib/motors/SparkFlexSwerve.java b/src/main/java/swervelib/motors/SparkFlexSwerve.java deleted file mode 100644 index 03b2e9ae..00000000 --- a/src/main/java/swervelib/motors/SparkFlexSwerve.java +++ /dev/null @@ -1,455 +0,0 @@ -package swervelib.motors; - -import static edu.wpi.first.units.Units.Milliseconds; -import static edu.wpi.first.units.Units.Seconds; - -import com.revrobotics.AbsoluteEncoder; -import com.revrobotics.REVLibError; -import com.revrobotics.RelativeEncoder; -import com.revrobotics.spark.ClosedLoopSlot; -import com.revrobotics.spark.SparkBase.ControlType; -import com.revrobotics.spark.SparkBase.PersistMode; -import com.revrobotics.spark.SparkBase.ResetMode; -import com.revrobotics.spark.SparkClosedLoopController; -import com.revrobotics.spark.SparkFlex; -import com.revrobotics.spark.SparkLowLevel.MotorType; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; -import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; -import com.revrobotics.spark.config.SparkFlexConfig; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.Timer; -import java.util.Optional; -import java.util.function.Supplier; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** An implementation of {@link SparkFlex} as a {@link SwerveMotor}. */ -public class SparkFlexSwerve extends SwerveMotor { - - /** Config retry delay. */ - private final double configDelay = Milliseconds.of(5).in(Seconds); - /** {@link SparkFlex} Instance. */ - private final SparkFlex motor; - /** Integrated encoder. */ - public RelativeEncoder encoder; - /** Absolute encoder attached to the SparkFlex (if exists) */ - public Optional absoluteEncoder = Optional.empty(); - /** Closed-loop PID controller. */ - public SparkClosedLoopController pid; - /** Supplier for the velocity of the motor controller. */ - private Supplier velocity; - /** Supplier for the position of the motor controller. */ - private Supplier position; - /** An {@link Alert} for if there is an error configuring the motor. */ - private Alert failureConfiguring; - /** Configuration object for {@link SparkFlex} motor. */ - private SparkFlexConfig cfg = new SparkFlexConfig(); - - /** - * Initialize the swerve motor. - * - * @param motor The SwerveMotor as a SparkFlex object. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param motorType {@link DCMotor} which the {@link SparkFlex} is attached to. - */ - public SparkFlexSwerve(SparkFlex motor, boolean isDriveMotor, DCMotor motorType) { - this.motor = motor; - this.isDriveMotor = isDriveMotor; - failureConfiguring = - new Alert("Motors", "Failure configuring motor " + motor.getDeviceId(), AlertType.kWarning); - factoryDefaults(); - clearStickyFaults(); - - encoder = motor.getEncoder(); - pid = motor.getClosedLoopController(); - cfg.closedLoop.feedbackSensor( - FeedbackSensor - .kPrimaryEncoder); // Configure feedback of the PID controller as the integrated - // encoder. - - // Spin off configurations in a different thread. - // configureSparkFlex(() -> motor.setCANTimeout(0)); // Commented out because it prevents - // feedback. - - velocity = encoder::getVelocity; - position = encoder::getPosition; - } - - /** - * Initialize the {@link SwerveMotor} as a {@link SparkFlex} connected to a Brushless Motor. - * - * @param id CAN ID of the SparkFlex. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param motorType {@link DCMotor} which the {@link SparkFlex} is attached to. - */ - public SparkFlexSwerve(int id, boolean isDriveMotor, DCMotor motorType) { - this(new SparkFlex(id, MotorType.kBrushless), isDriveMotor, motorType); - } - - /** - * Run the configuration until it succeeds or times out. - * - * @param config Lambda supplier returning the error state. - */ - private void configureSparkFlex(Supplier config) { - for (int i = 0; i < maximumRetries; i++) { - if (config.get() == REVLibError.kOk) { - return; - } - Timer.delay(configDelay); - } - failureConfiguring.set(true); - } - - @Override - public void close() { - motor.close(); - } - - /** - * Get the current configuration of the {@link SparkFlex} - * - * @return {@link SparkFlexConfig} - */ - public SparkFlexConfig getConfig() { - return cfg; - } - - /** - * Update the config for the {@link SparkFlex} - * - * @param cfgGiven Given {@link SparkFlexConfig} which should have minimal modifications. - */ - public void updateConfig(SparkFlexConfig cfgGiven) { - if (!DriverStation.isDisabled()) { - DriverStation.reportWarning( - "Configuration changes cannot be applied while the robot is enabled.", false); - } - cfg.apply(cfgGiven); - configureSparkFlex( - () -> - motor.configure(cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters)); - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - cfg.voltageCompensation(nominalVoltage); - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - - cfg.smartCurrentLimit(currentLimit); - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - cfg.closedLoopRampRate(rampRate).openLoopRampRate(rampRate); - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - @Override - public DCMotor getSimMotor() { - if (simMotor == null) { - simMotor = DCMotor.getNeoVortex(1); - } - return simMotor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder.isPresent(); - } - - /** Configure the factory defaults. */ - @Override - public void factoryDefaults() { - // Do nothing - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - configureSparkFlex(motor::clearFaults); - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - * @return The {@link SwerveMotor} for easy instantiation. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - if (encoder == null) { - this.absoluteEncoder = Optional.empty(); - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - - velocity = this.encoder::getVelocity; - position = this.encoder::getPosition; - } else if (encoder.getAbsoluteEncoder() instanceof AbsoluteEncoder) { - cfg.closedLoop.feedbackSensor(FeedbackSensor.kAbsoluteEncoder); - this.absoluteEncoder = Optional.of(encoder); - - velocity = this.absoluteEncoder.get()::getVelocity; - position = this.absoluteEncoder.get()::getAbsolutePosition; - } - return this; - } - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply. - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - cfg.signals - .absoluteEncoderPositionAlwaysOn(false) - .absoluteEncoderVelocityAlwaysOn(false) - .analogPositionAlwaysOn(false) - .analogVelocityAlwaysOn(false) - .analogVoltageAlwaysOn(false) - .externalOrAltEncoderPositionAlwaysOn(false) - .externalOrAltEncoderVelocityAlwaysOn(false) - .primaryEncoderPositionAlwaysOn(false) - .primaryEncoderVelocityAlwaysOn(false) - .iAccumulationAlwaysOn(false) - .appliedOutputPeriodMs(10) - .faultsPeriodMs(20); - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - - cfg.encoder - .positionConversionFactor(positionConversionFactor) - .velocityConversionFactor(positionConversionFactor / 60); - // Changes the measurement period and number of samples used to calculate the velocity for the - // intergrated motor controller - // Notability this changes the returned velocity and the velocity used for the onboard velocity - // PID loop (TODO: triple check the PID portion of this statement) - // Default settings of 32ms and 8 taps introduce ~100ms of measurement lag - // https://www.chiefdelphi.com/t/shooter-encoder/400211/11 - // This value was taken from: - // https://github.com/Mechanical-Advantage/RobotCode2023/blob/9884d13b2220b76d430e82248fd837adbc4a10bc/src/main/java/org/littletonrobotics/frc2023/subsystems/drive/ModuleIOSparkMax.java#L132-L133 - // and tested on 9176 for YAGSL, notably 3005 uses 16ms instead of 10 but 10 is more common - // based on github searches - cfg.encoder.quadratureMeasurementPeriod(10).quadratureAverageDepth(2); - - // Taken from - // https://github.com/frc3512/SwerveBot-2022/blob/9d31afd05df6c630d5acb4ec2cf5d734c9093bf8/src/main/java/frc/lib/util/SparkMaxUtil.java#L67 - // Unused frames can be set to 65535 to decrease CAN ultilization. - cfg.signals - .primaryEncoderVelocityAlwaysOn( - isDriveMotor) // Disable velocity reporting for angle motors. - .primaryEncoderPositionAlwaysOn(true) - .primaryEncoderPositionPeriodMs(20); - } - - /** - * Configure the PIDF values for the closed loop controller. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - cfg.closedLoop - .pidf(config.p, config.i, config.d, config.f) - .iZone(config.iz) - .outputRange(config.output.min, config.output.max); - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - cfg.closedLoop.positionWrappingEnabled(true).positionWrappingInputRange(minInput, maxInput); - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - cfg.closedLoop.positionWrappingEnabled(false); - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - cfg.idleMode(isBrakeMode ? IdleMode.kBrake : IdleMode.kCoast); - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - cfg.inverted(inverted); - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - configureSparkFlex( - () -> { - return motor.configure( - cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters); - }); - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.set(percentOutput); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - - if (isDriveMotor) { - configureSparkFlex( - () -> - pid.setReference( - setpoint, ControlType.kVelocity, ClosedLoopSlot.kSlot0, feedforward)); - } else { - configureSparkFlex( - () -> - pid.setReference( - setpoint, ControlType.kPosition, ClosedLoopSlot.kSlot0, feedforward)); - if (SwerveDriveTelemetry.isSimulation) { - encoder.setPosition(setpoint); - } - } - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - setReference(setpoint, feedforward); - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getAppliedOutput() * motor.getBusVoltage(); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - @Override - public double getAppliedOutput() { - return motor.getAppliedOutput(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity - */ - @Override - public double getVelocity() { - return velocity.get(); - } - - /** - * Get the position of the integrated encoder. - * - * @return Position - */ - @Override - public double getPosition() { - return position.get(); - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. - */ - @Override - public void setPosition(double position) { - if (absoluteEncoder.isEmpty()) { - configureSparkFlex(() -> encoder.setPosition(position)); - } - } -} diff --git a/src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java b/src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java deleted file mode 100644 index 98b73b30..00000000 --- a/src/main/java/swervelib/motors/SparkMaxBrushedMotorSwerve.java +++ /dev/null @@ -1,555 +0,0 @@ -package swervelib.motors; - -import static edu.wpi.first.units.Units.Milliseconds; -import static edu.wpi.first.units.Units.Seconds; - -import com.revrobotics.REVLibError; -import com.revrobotics.RelativeEncoder; -import com.revrobotics.spark.ClosedLoopSlot; -import com.revrobotics.spark.SparkBase.ControlType; -import com.revrobotics.spark.SparkBase.PersistMode; -import com.revrobotics.spark.SparkBase.ResetMode; -import com.revrobotics.spark.SparkClosedLoopController; -import com.revrobotics.spark.SparkLowLevel.MotorType; -import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; -import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; -import com.revrobotics.spark.config.SparkMaxConfig; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.Timer; -import java.util.Optional; -import java.util.function.Supplier; -import swervelib.encoders.SparkMaxAnalogEncoderSwerve; -import swervelib.encoders.SparkMaxEncoderSwerve; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** Brushed motor control with {@link SparkMax}. */ -public class SparkMaxBrushedMotorSwerve extends SwerveMotor { - - /** Config retry delay. */ - private final double configDelay = Milliseconds.of(5).in(Seconds); - /** SparkMAX Instance. */ - private final SparkMax motor; - /** Absolute encoder attached to the SparkMax (if exists) */ - public Optional absoluteEncoder = Optional.empty(); - /** Integrated encoder. */ - public Optional encoder = Optional.empty(); - /** Closed-loop PID controller. */ - public SparkClosedLoopController pid; - /** Supplier for the velocity of the motor controller. */ - private Supplier velocity; - /** Supplier for the position of the motor controller. */ - private Supplier position; - /** An {@link Alert} for if the motor has no encoder. */ - private Alert noEncoderAlert; - /** An {@link Alert} for if there is an error configuring the motor. */ - private Alert failureConfiguringAlert; - /** An {@link Alert} for if the motor has no encoder defined. */ - private Alert noEncoderDefinedAlert; - /** Configuration object for {@link SparkMax} motor. */ - private SparkMaxConfig cfg = new SparkMaxConfig(); - - /** - * Initialize the swerve motor. - * - * @param motor The SwerveMotor as a SparkMax object. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param encoderType {@link Type} of encoder to use for the {@link SparkMax} device. - * @param countsPerRevolution The number of encoder pulses for the {@link Type} encoder per - * revolution. - * @param useDataPortQuadEncoder Use the encoder attached to the data port of the spark max for a - * quadrature encoder. - * @param motorType {@link DCMotor} which the {@link SparkMax} is attached to. - */ - public SparkMaxBrushedMotorSwerve( - SparkMax motor, - boolean isDriveMotor, - Type encoderType, - int countsPerRevolution, - boolean useDataPortQuadEncoder, - DCMotor motorType) { - noEncoderAlert = new Alert("Motors", "Cannot use motor without encoder.", AlertType.kError); - failureConfiguringAlert = - new Alert("Motors", "Failure configuring motor " + motor.getDeviceId(), AlertType.kWarning); - noEncoderDefinedAlert = - new Alert("Motors", "An encoder MUST be defined to work with a SparkMAX", AlertType.kError); - - // Drive motors **MUST** have an encoder attached. - if (isDriveMotor && encoderType == Type.kNoSensor) { - noEncoderAlert.set(true); - throw new RuntimeException( - "Cannot use SparkMAX as a drive motor without an encoder attached."); - } - - // Hall encoders can be used as quadrature encoders. - if (encoderType == Type.kHallSensor) { - encoderType = Type.kQuadrature; - } - - this.motor = motor; - this.isDriveMotor = isDriveMotor; - this.simMotor = motorType; - - factoryDefaults(); - clearStickyFaults(); - - // Get the onboard PID controller. - pid = motor.getClosedLoopController(); - - // If there is a sensor attached to the data port or encoder port set the relative encoder. - if (isDriveMotor || (encoderType != Type.kNoSensor || useDataPortQuadEncoder)) { - - if (useDataPortQuadEncoder) { - this.encoder = Optional.of(motor.getAlternateEncoder()); - cfg.alternateEncoder.countsPerRevolution(countsPerRevolution); - - // Configure feedback of the PID controller as the integrated encoder. - cfg.closedLoop.feedbackSensor(FeedbackSensor.kAlternateOrExternalEncoder); - } else { - this.encoder = Optional.of(motor.getEncoder()); - cfg.encoder.countsPerRevolution(countsPerRevolution); - - // Configure feedback of the PID controller as the integrated encoder. - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - } - } - encoder.ifPresentOrElse( - (RelativeEncoder enc) -> { - velocity = enc::getVelocity; - position = enc::getPosition; - }, - () -> { - noEncoderDefinedAlert.set(true); - }); - // Spin off configurations in a different thread. - // configureSparkMax(() -> motor.setCANTimeout(0)); // Commented it out because it prevents - // feedback. - - } - - /** - * Initialize the {@link SwerveMotor} as a {@link SparkMax} connected to a Brushless Motor. - * - * @param id CAN ID of the SparkMax. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param encoderType {@link Type} of encoder to use for the {@link SparkMax} device. - * @param countsPerRevolution The number of encoder pulses for the {@link Type} encoder per - * revolution. - * @param useDataPortQuadEncoder Use the encoder attached to the data port of the spark max for a - * quadrature encoder. - * @param motorType Motor type controlled by the {@link SparkMax} motor controller. - */ - public SparkMaxBrushedMotorSwerve( - int id, - boolean isDriveMotor, - Type encoderType, - int countsPerRevolution, - boolean useDataPortQuadEncoder, - DCMotor motorType) { - this( - new SparkMax(id, MotorType.kBrushed), - isDriveMotor, - encoderType, - countsPerRevolution, - useDataPortQuadEncoder, - motorType); - } - - /** - * Run the configuration until it succeeds or times out. - * - * @param config Lambda supplier returning the error state. - */ - private void configureSparkMax(Supplier config) { - for (int i = 0; i < maximumRetries; i++) { - if (config.get() == REVLibError.kOk) { - return; - } - Timer.delay(configDelay); - } - failureConfiguringAlert.set(true); - } - - @Override - public void close() { - motor.close(); - } - - /** - * Get the current configuration of the {@link SparkMax} - * - * @return {@link SparkMaxConfig} - */ - public SparkMaxConfig getConfig() { - return cfg; - } - - /** - * Update the config for the {@link SparkMax} - * - * @param cfgGiven Given {@link SparkMaxConfig} which should have minimal modifications. - */ - public void updateConfig(SparkMaxConfig cfgGiven) { - cfg.apply(cfgGiven); - configureSparkMax( - () -> - motor.configure(cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters)); - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - cfg.voltageCompensation(nominalVoltage); - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - cfg.smartCurrentLimit(currentLimit); - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - cfg.closedLoopRampRate(rampRate).openLoopRampRate(rampRate); - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - @Override - public DCMotor getSimMotor() { - if (simMotor == null) { - simMotor = DCMotor.getCIM(1); - } - return simMotor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder.isPresent(); - } - - /** Configure the factory defaults. */ - @Override - public void factoryDefaults() { - // Do nothing - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - configureSparkMax(motor::clearFaults); - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - * @return The {@link SwerveMotor} for easy instantiation. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - if (encoder == null) { - this.absoluteEncoder = Optional.empty(); - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - - this.encoder.ifPresentOrElse( - (RelativeEncoder enc) -> { - velocity = enc::getVelocity; - position = enc::getPosition; - }, - () -> { - noEncoderDefinedAlert.set(true); - }); - burnFlash(); - } else if (encoder instanceof SparkMaxAnalogEncoderSwerve - || encoder instanceof SparkMaxEncoderSwerve) { - cfg.closedLoop.feedbackSensor( - encoder instanceof SparkMaxAnalogEncoderSwerve - ? FeedbackSensor.kAnalogSensor - : FeedbackSensor.kAbsoluteEncoder); - - this.absoluteEncoder = Optional.of(encoder); - velocity = this.absoluteEncoder.get()::getVelocity; - position = this.absoluteEncoder.get()::getAbsolutePosition; - noEncoderDefinedAlert.set(false); - } - if (absoluteEncoder.isEmpty() && this.encoder.isEmpty()) { - noEncoderDefinedAlert.set(true); - throw new RuntimeException("An encoder MUST be defined to work with a SparkMAX"); - } - return this; - } - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply. - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - cfg.signals - .absoluteEncoderPositionAlwaysOn(false) - .absoluteEncoderVelocityAlwaysOn(false) - .analogPositionAlwaysOn(false) - .analogVelocityAlwaysOn(false) - .analogVoltageAlwaysOn(false) - .externalOrAltEncoderPositionAlwaysOn(false) - .externalOrAltEncoderVelocityAlwaysOn(false) - .primaryEncoderPositionAlwaysOn(false) - .primaryEncoderVelocityAlwaysOn(false) - .iAccumulationAlwaysOn(false) - .appliedOutputPeriodMs(10) - .faultsPeriodMs(20); - - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - cfg.encoder - .positionConversionFactor(positionConversionFactor) - .velocityConversionFactor(positionConversionFactor / 60); - // Changes the measurement period and number of samples used to calculate the velocity for the - // intergrated motor controller - // Notability this changes the returned velocity and the velocity used for the onboard velocity - // PID loop (TODO: triple check the PID portion of this statement) - // Default settings of 32ms and 8 taps introduce ~100ms of measurement lag - // https://www.chiefdelphi.com/t/shooter-encoder/400211/11 - // This value was taken from: - // https://github.com/Mechanical-Advantage/RobotCode2023/blob/9884d13b2220b76d430e82248fd837adbc4a10bc/src/main/java/org/littletonrobotics/frc2023/subsystems/drive/ModuleIOSparkMax.java#L132-L133 - // and tested on 9176 for YAGSL, notably 3005 uses 16ms instead of 10 but 10 is more common - // based on github searches - cfg.encoder.quadratureMeasurementPeriod(10).quadratureAverageDepth(2); - - // Taken from - // https://github.com/frc3512/SwerveBot-2022/blob/9d31afd05df6c630d5acb4ec2cf5d734c9093bf8/src/main/java/frc/lib/util/SparkMaxUtil.java#L67 - // Unused frames can be set to 65535 to decrease CAN ultilization. - cfg.signals - .primaryEncoderVelocityAlwaysOn( - isDriveMotor) // Disable velocity reporting for angle motors. - .primaryEncoderPositionAlwaysOn(true) - .primaryEncoderPositionPeriodMs(20); - } - - /** - * Configure the PIDF values for the closed loop controller. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - cfg.closedLoop - .pidf(config.p, config.i, config.d, config.f) - .iZone(config.iz) - .outputRange(config.output.min, config.output.max); - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - cfg.closedLoop.positionWrappingEnabled(true).positionWrappingInputRange(minInput, maxInput); - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - cfg.closedLoop.positionWrappingEnabled(false); - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - cfg.idleMode(isBrakeMode ? IdleMode.kBrake : IdleMode.kCoast); - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - cfg.inverted(inverted); - if (isDriveMotor) { - cfg.encoder.inverted(inverted); - } - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - configureSparkMax( - () -> { - return motor.configure( - cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters); - }); - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.set(percentOutput); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - int pidSlot = 0; - - if (isDriveMotor) { - configureSparkMax( - () -> - pid.setReference( - setpoint, ControlType.kVelocity, ClosedLoopSlot.kSlot0, feedforward)); - } else { - configureSparkMax( - () -> - pid.setReference( - setpoint, ControlType.kPosition, ClosedLoopSlot.kSlot0, feedforward)); - if (SwerveDriveTelemetry.isSimulation) { - encoder.ifPresent( - (RelativeEncoder enc) -> { - enc.setPosition(setpoint); - }); - } - } - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - setReference(setpoint, feedforward); - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getAppliedOutput() * motor.getBusVoltage(); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - @Override - public double getAppliedOutput() { - return motor.getAppliedOutput(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity - */ - @Override - public double getVelocity() { - return velocity.get(); - } - - /** - * Get the position of the integrated encoder. - * - * @return Position - */ - @Override - public double getPosition() { - return position.get(); - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. - */ - @Override - public void setPosition(double position) { - if (absoluteEncoder.isEmpty()) { - encoder.ifPresent( - (RelativeEncoder enc) -> { - configureSparkMax(() -> enc.setPosition(position)); - }); - } - } - - /** Type for encoder for {@link SparkMax} */ - public enum Type { - /** NO sensor */ - kNoSensor, - /** Hall sensor attached to dataport */ - kHallSensor, - /** Quad encoder attached to alt */ - kQuadrature, - } -} diff --git a/src/main/java/swervelib/motors/SparkMaxSwerve.java b/src/main/java/swervelib/motors/SparkMaxSwerve.java deleted file mode 100644 index e70e12c4..00000000 --- a/src/main/java/swervelib/motors/SparkMaxSwerve.java +++ /dev/null @@ -1,456 +0,0 @@ -package swervelib.motors; - -import static edu.wpi.first.units.Units.Milliseconds; -import static edu.wpi.first.units.Units.Seconds; - -import com.revrobotics.REVLibError; -import com.revrobotics.RelativeEncoder; -import com.revrobotics.spark.ClosedLoopSlot; -import com.revrobotics.spark.SparkBase.ControlType; -import com.revrobotics.spark.SparkBase.PersistMode; -import com.revrobotics.spark.SparkBase.ResetMode; -import com.revrobotics.spark.SparkClosedLoopController; -import com.revrobotics.spark.SparkLowLevel.MotorType; -import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; -import com.revrobotics.spark.config.SparkBaseConfig.IdleMode; -import com.revrobotics.spark.config.SparkMaxConfig; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.Timer; -import java.util.Optional; -import java.util.function.Supplier; -import swervelib.encoders.SparkMaxAnalogEncoderSwerve; -import swervelib.encoders.SparkMaxEncoderSwerve; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** An implementation of {@link com.revrobotics.spark.SparkMax} as a {@link SwerveMotor}. */ -public class SparkMaxSwerve extends SwerveMotor { - - /** Config retry delay. */ - private final double configDelay = Milliseconds.of(5).in(Seconds); - /** {@link SparkMax} Instance. */ - private final SparkMax motor; - /** Integrated encoder. */ - public RelativeEncoder encoder; - /** Closed-loop PID controller. */ - public SparkClosedLoopController pid; - /** Absolute encoder attached to the SparkMax (if exists) */ - private Optional absoluteEncoder = Optional.empty(); - /** Supplier for the velocity of the motor controller. */ - private Supplier velocity; - /** Supplier for the position of the motor controller. */ - private Supplier position; - /** Configuration object for {@link SparkMax} motor. */ - private SparkMaxConfig cfg = new SparkMaxConfig(); - - /** - * Initialize the swerve motor. - * - * @param motor The SwerveMotor as a SparkMax object. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param motorType Motor type controlled by the {@link SparkMax} motor controller. - */ - public SparkMaxSwerve(SparkMax motor, boolean isDriveMotor, DCMotor motorType) { - this.motor = motor; - this.isDriveMotor = isDriveMotor; - this.simMotor = motorType; - factoryDefaults(); - clearStickyFaults(); - - encoder = motor.getEncoder(); - pid = motor.getClosedLoopController(); - - cfg.closedLoop.feedbackSensor( - FeedbackSensor - .kPrimaryEncoder); // Configure feedback of the PID controller as the integrated - // encoder. - velocity = encoder::getVelocity; - position = encoder::getPosition; - - // Spin off configurations in a different thread. - // configureSparkMax(() -> motor.setCANTimeout(0)); // Commented out because it prevents - // feedback. - } - - /** - * Initialize the {@link SwerveMotor} as a {@link SparkMax} connected to a Brushless Motor. - * - * @param id CAN ID of the SparkMax. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param motorType Motor type controlled by the {@link SparkMax} motor controller. - */ - public SparkMaxSwerve(int id, boolean isDriveMotor, DCMotor motorType) { - this(new SparkMax(id, MotorType.kBrushless), isDriveMotor, motorType); - } - - /** - * Run the configuration until it succeeds or times out. - * - * @param config Lambda supplier returning the error state. - */ - private void configureSparkMax(Supplier config) { - for (int i = 0; i < maximumRetries; i++) { - if (config.get() == REVLibError.kOk) { - return; - } - Timer.delay(configDelay); - } - DriverStation.reportWarning("Failure configuring motor " + motor.getDeviceId(), true); - } - - @Override - public void close() { - motor.close(); - } - - /** - * Get the current configuration of the {@link SparkMax} - * - * @return {@link SparkMaxConfig} - */ - public SparkMaxConfig getConfig() { - return cfg; - } - - /** - * Update the config for the {@link SparkMax} - * - * @param cfgGiven Given {@link SparkMaxConfig} which should have minimal modifications. - */ - public void updateConfig(SparkMaxConfig cfgGiven) { - if (!DriverStation.isDisabled()) { - DriverStation.reportWarning( - "Configuration changes cannot be applied while the robot is enabled.", false); - } - cfg.apply(cfgGiven); - configureSparkMax( - () -> - motor.configure(cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters)); - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - cfg.voltageCompensation(nominalVoltage); - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - cfg.smartCurrentLimit(currentLimit); - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - cfg.closedLoopRampRate(rampRate).openLoopRampRate(rampRate); - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - @Override - public DCMotor getSimMotor() { - if (simMotor == null) { - simMotor = DCMotor.getNEO(1); - } - return simMotor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder.isPresent(); - } - - /** Configure the factory defaults. */ - @Override - public void factoryDefaults() { - // Do nothing - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - configureSparkMax(motor::clearFaults); - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - * @return The {@link SwerveMotor} for easy instantiation. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - if (encoder == null) { - this.absoluteEncoder = Optional.empty(); - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - - velocity = this.encoder::getVelocity; - position = this.encoder::getPosition; - burnFlash(); - } else if (encoder instanceof SparkMaxAnalogEncoderSwerve - || encoder instanceof SparkMaxEncoderSwerve) { - cfg.closedLoop.feedbackSensor( - encoder instanceof SparkMaxAnalogEncoderSwerve - ? FeedbackSensor.kAnalogSensor - : FeedbackSensor.kAbsoluteEncoder); - - this.absoluteEncoder = Optional.of(encoder); - velocity = this.absoluteEncoder.get()::getVelocity; - position = this.absoluteEncoder.get()::getAbsolutePosition; - } - return this; - } - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply. - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - cfg.signals - .absoluteEncoderPositionAlwaysOn(false) - .absoluteEncoderVelocityAlwaysOn(false) - .analogPositionAlwaysOn(false) - .analogVelocityAlwaysOn(false) - .analogVoltageAlwaysOn(false) - .externalOrAltEncoderPositionAlwaysOn(false) - .externalOrAltEncoderVelocityAlwaysOn(false) - .primaryEncoderPositionAlwaysOn(false) - .primaryEncoderVelocityAlwaysOn(false) - .iAccumulationAlwaysOn(false) - .appliedOutputPeriodMs(10) - .faultsPeriodMs(20); - - cfg.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder); - cfg.encoder - .positionConversionFactor(positionConversionFactor) - .velocityConversionFactor(positionConversionFactor / 60); - // Changes the measurement period and number of samples used to calculate the velocity for the - // intergrated motor controller - // Notability this changes the returned velocity and the velocity used for the onboard velocity - // PID loop (TODO: triple check the PID portion of this statement) - // Default settings of 32ms and 8 taps introduce ~100ms of measurement lag - // https://www.chiefdelphi.com/t/shooter-encoder/400211/11 - // This value was taken from: - // https://github.com/Mechanical-Advantage/RobotCode2023/blob/9884d13b2220b76d430e82248fd837adbc4a10bc/src/main/java/org/littletonrobotics/frc2023/subsystems/drive/ModuleIOSparkMax.java#L132-L133 - // and tested on 9176 for YAGSL, notably 3005 uses 16ms instead of 10 but 10 is more common - // based on github searches - cfg.encoder.quadratureMeasurementPeriod(10).quadratureAverageDepth(2); - - // Taken from - // https://github.com/frc3512/SwerveBot-2022/blob/9d31afd05df6c630d5acb4ec2cf5d734c9093bf8/src/main/java/frc/lib/util/SparkMaxUtil.java#L67 - // Unused frames can be set to 65535 to decrease CAN ultilization. - cfg.signals - .primaryEncoderVelocityAlwaysOn( - isDriveMotor) // Disable velocity reporting for angle motors. - .primaryEncoderPositionAlwaysOn(true) - .primaryEncoderPositionPeriodMs(20); - } - - /** - * Configure the PIDF values for the closed loop controller. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - cfg.closedLoop - .pidf(config.p, config.i, config.d, config.f) - .iZone(config.iz) - .outputRange(config.output.min, config.output.max); - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - cfg.closedLoop.positionWrappingEnabled(true).positionWrappingInputRange(minInput, maxInput); - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - cfg.closedLoop.positionWrappingEnabled(false); - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - cfg.idleMode(isBrakeMode ? IdleMode.kBrake : IdleMode.kCoast); - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - cfg.inverted(inverted); - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - configureSparkMax( - () -> { - return motor.configure( - cfg, ResetMode.kNoResetSafeParameters, PersistMode.kPersistParameters); - }); - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.set(percentOutput); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - int pidSlot = 0; - - if (isDriveMotor) { - configureSparkMax( - () -> - pid.setReference( - setpoint, ControlType.kVelocity, ClosedLoopSlot.kSlot0, feedforward)); - } else { - configureSparkMax( - () -> - pid.setReference( - setpoint, ControlType.kPosition, ClosedLoopSlot.kSlot0, feedforward)); - if (SwerveDriveTelemetry.isSimulation) { - encoder.setPosition(setpoint); - } - } - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - setReference(setpoint, feedforward); - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getAppliedOutput() * motor.getBusVoltage(); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - @Override - public double getAppliedOutput() { - return motor.getAppliedOutput(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity - */ - @Override - public double getVelocity() { - return velocity.get(); - } - - /** - * Get the position of the integrated encoder. - * - * @return Position - */ - @Override - public double getPosition() { - return position.get(); - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. - */ - @Override - public void setPosition(double position) { - if (absoluteEncoder.isEmpty()) { - configureSparkMax(() -> encoder.setPosition(position)); - } - } -} diff --git a/src/main/java/swervelib/motors/SwerveMotor.java b/src/main/java/swervelib/motors/SwerveMotor.java deleted file mode 100644 index d139d36a..00000000 --- a/src/main/java/swervelib/motors/SwerveMotor.java +++ /dev/null @@ -1,195 +0,0 @@ -package swervelib.motors; - -import edu.wpi.first.math.system.plant.DCMotor; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; - -/** - * Swerve motor abstraction which defines a standard interface for motors within a swerve module. - */ -public abstract class SwerveMotor implements AutoCloseable { - - @Override - public abstract void close(); - - /** - * The maximum amount of times the swerve motor will attempt to configure a motor if failures - * occur. - */ - public final int maximumRetries = 5; - /** - * Sim motor to use, defaulted in {@link SwerveMotor#getSimMotor()}, but can be overridden here. - *
- * NOTE: This will not change the simulation motor type! It is intended for use only if you are - * utilizing Feedforwards from PathPlanner. - */ - public DCMotor simMotor; - /** Whether the swerve motor is a drive motor. */ - protected boolean isDriveMotor; - - /** Configure the factory defaults. */ - public abstract void factoryDefaults(); - - /** Clear the sticky faults on the motor controller. */ - public abstract void clearStickyFaults(); - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - * @return The {@link SwerveMotor} for single line configuration. - */ - public abstract SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder); - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply for position. - */ - public abstract void configureIntegratedEncoder(double positionConversionFactor); - - /** - * Configure the PIDF values for the closed loop controller. 0 is disabled or off. - * - * @param config Configuration class holding the PIDF values. - */ - public abstract void configurePIDF(PIDFConfig config); - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - public abstract void configurePIDWrapping(double minInput, double maxInput); - - /** Disable PID Wrapping on the motor. */ - public abstract void disablePIDWrapping(); - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - public abstract void setMotorBrake(boolean isBrakeMode); - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - public abstract void setInverted(boolean inverted); - - /** Save the configurations from flash to EEPROM. */ - public abstract void burnFlash(); - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - public abstract void set(double percentOutput); - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - public abstract void setReference(double setpoint, double feedforward); - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - public abstract void setReference(double setpoint, double feedforward, double position); - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - public abstract double getVoltage(); - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - public abstract void setVoltage(double voltage); - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - public abstract double getAppliedOutput(); - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity in meters per second or degrees per second. - */ - public abstract double getVelocity(); - - /** - * Get the position of the integrated encoder. - * - * @return Position in meters or degrees. - */ - public abstract double getPosition(); - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. Should be angle in degrees or meters per second. - */ - public abstract void setPosition(double position); - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - public abstract void setVoltageCompensation(double nominalVoltage); - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - public abstract void setCurrentLimit(int currentLimit); - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - public abstract void setLoopRampRate(double rampRate); - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - public abstract Object getMotor(); - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - public abstract DCMotor getSimMotor(); - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - public abstract boolean usingExternalFeedbackSensor(); -} diff --git a/src/main/java/swervelib/motors/TalonFXSSwerve.java b/src/main/java/swervelib/motors/TalonFXSSwerve.java deleted file mode 100644 index 248bf0f2..00000000 --- a/src/main/java/swervelib/motors/TalonFXSSwerve.java +++ /dev/null @@ -1,399 +0,0 @@ -package swervelib.motors; - -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Rotations; -import static edu.wpi.first.units.Units.Volts; - -import com.ctre.phoenix6.configs.TalonFXSConfiguration; -import com.ctre.phoenix6.configs.TalonFXSConfigurator; -import com.ctre.phoenix6.controls.MotionMagicVoltage; -import com.ctre.phoenix6.controls.VelocityVoltage; -import com.ctre.phoenix6.hardware.TalonFXS; -import com.ctre.phoenix6.signals.InvertedValue; -import com.ctre.phoenix6.signals.NeutralModeValue; -import edu.wpi.first.math.system.plant.DCMotor; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** {@link TalonFXS} Swerve Motor. Made by Team 1466 WebbRobotics. */ -public class TalonFXSSwerve extends SwerveMotor { - - /** Wait time for status frames to show up. */ - public static double STATUS_TIMEOUT_SECONDS = 0.02; - /** Factory default already occurred. */ - private final boolean factoryDefaultOccurred = false; - /** Whether the absolute encoder is integrated. */ - private final boolean absoluteEncoder = false; - /** Motion magic angle voltage setter. */ - private final MotionMagicVoltage m_angleVoltageSetter = new MotionMagicVoltage(0); - /** Velocity voltage setter for controlling drive motor. */ - private final VelocityVoltage m_velocityVoltageSetter = new VelocityVoltage(0); - /** TalonFXS motor controller. */ - private final TalonFXS motor; - /** Conversion factor for the motor. */ - private double conversionFactor; - /** Current TalonFXS configuration. */ - private TalonFXSConfiguration configuration = new TalonFXSConfiguration(); - /** Current TalonFXS Configurator. */ - private TalonFXSConfigurator cfg; - - /** - * Constructor for TalonFXS swerve motor. - * - * @param motor Motor to use. - * @param isDriveMotor Whether this motor is a drive motor. - * @param motorType {@link DCMotor} which the {@link TalonFXS} is attached to. - */ - public TalonFXSSwerve(TalonFXS motor, boolean isDriveMotor, DCMotor motorType) { - this.isDriveMotor = isDriveMotor; - this.motor = motor; - this.cfg = motor.getConfigurator(); - this.simMotor = motorType; - - factoryDefaults(); - clearStickyFaults(); - } - - /** - * Construct the TalonFXS swerve motor given the ID and CANBus. - * - * @param id ID of the TalonFXS on the CANBus. - * @param canbus CANBus on which the TalonFXS is on. - * @param isDriveMotor Whether the motor is a drive or steering motor. - * @param motorType {@link DCMotor} which the {@link TalonFXS} is attached to. - */ - public TalonFXSSwerve(int id, String canbus, boolean isDriveMotor, DCMotor motorType) { - this(new TalonFXS(id, canbus), isDriveMotor, motorType); - } - - /** - * Construct the TalonFXS swerve motor given the ID. - * - * @param id ID of the TalonFXS on the canbus. - * @param isDriveMotor Whether the motor is a drive or steering motor. - * @param motorType {@link DCMotor} which the {@link TalonFXS} is attached to. - */ - public TalonFXSSwerve(int id, boolean isDriveMotor, DCMotor motorType) { - this(new TalonFXS(id), isDriveMotor, motorType); - } - - /** Configure the factory defaults. */ - @Override - public void factoryDefaults() { - if (!factoryDefaultOccurred) { - configuration.MotorOutput.NeutralMode = NeutralModeValue.Brake; - configuration.ClosedLoopGeneral.ContinuousWrap = true; - cfg.apply(configuration); - - m_angleVoltageSetter.UpdateFreqHz = 0; - // m_angleVoltageExpoSetter.UpdateFreqHz = 0; - m_velocityVoltageSetter.UpdateFreqHz = 0; - // motor.configFactoryDefault(); - // motor.setSensorPhase(true); - // motor.configSelectedFeedbackSensor(TalonFXSFeedbackDevice.IntegratedSensor, 0, 30); - // motor.configNeutralDeadband(0.001); - } - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - motor.clearStickyFaults(); - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - // Do not support. - return this; - } - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply for position. - *


- * Degrees:
- * - * 360 / (angleGearRatio * encoderTicksPerRotation) - *
- *


- * Meters:
- * - * (Math.PI * wheelDiameter) / (driveGearRatio * encoderTicksPerRotation) - * - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - cfg.refresh(configuration); - - positionConversionFactor = 1 / positionConversionFactor; - if (!isDriveMotor) { - positionConversionFactor *= 360; - } - conversionFactor = positionConversionFactor; - - configuration.MotionMagic = - configuration.MotionMagic.withMotionMagicCruiseVelocity(100.0 / positionConversionFactor) - .withMotionMagicAcceleration((100.0 / positionConversionFactor) / 0.100) - .withMotionMagicExpo_kV(0.12 * positionConversionFactor) - .withMotionMagicExpo_kA(0.1); - - /* - configuration.Feedback.withFeedbackSensorSource(FeedbackSensorSourceValue.RotorSensor) - .withSensorToMechanismRatio(positionConversionFactor); - */ - - cfg.apply(configuration); - } - - /** - * Configure the PIDF values for the closed loop controller. 0 is disabled or off. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - - cfg.refresh(configuration.Slot0); - cfg.apply( - configuration.Slot0.withKP(config.p).withKI(config.i).withKD(config.d).withKS(config.f)); - // configuration.slot0.integralZone = config.iz; - // configuration.slot0.closedLoopPeakOutput = config.output.max; - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - cfg.refresh(configuration.ClosedLoopGeneral); - configuration.ClosedLoopGeneral.ContinuousWrap = true; - cfg.apply(configuration.ClosedLoopGeneral); - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - cfg.refresh(configuration.ClosedLoopGeneral); - configuration.ClosedLoopGeneral.ContinuousWrap = false; - cfg.apply(configuration.ClosedLoopGeneral); - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - motor.setNeutralMode(isBrakeMode ? NeutralModeValue.Brake : NeutralModeValue.Coast); - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - // Timer.delay(1); - cfg.refresh(configuration.MotorOutput); - configuration.MotorOutput.withInverted( - inverted ? InvertedValue.CounterClockwise_Positive : InvertedValue.Clockwise_Positive); - cfg.apply(configuration.MotorOutput); - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - // Do nothing - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.set(percentOutput); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - setReference(setpoint, feedforward, getPosition()); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - // if (SwerveDriveTelemetry.isSimulation) - // { - // PhysicsSim.getInstance().run(); - // } - - if (isDriveMotor) { - motor.setControl(m_velocityVoltageSetter.withVelocity(setpoint).withFeedForward(feedforward)); - } else { - motor.setControl(m_angleVoltageSetter.withPosition(setpoint / 360.0)); - } - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getMotorVoltage().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue().in(Volts); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - @Override - public double getAppliedOutput() { - return motor.getDutyCycle().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity in Meters Per Second, or Degrees per Second. - */ - @Override - public double getVelocity() { - return motor.getVelocity().getValue().magnitude(); - } - - /** - * Get the position of the integrated encoder. - * - * @return Position in Meters or Degrees. - */ - @Override - public double getPosition() { - return motor.getPosition().getValue().magnitude(); - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. Should be angle in degrees or meters. - */ - @Override - public void setPosition(double position) { - if (!absoluteEncoder && !SwerveDriveTelemetry.isSimulation) { - cfg.setPosition(Degrees.of(position).in(Rotations)); - } - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - // Do not implement - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - cfg.refresh(configuration.CurrentLimits); - cfg.apply( - configuration.CurrentLimits.withSupplyCurrentLimit(currentLimit) - .withSupplyCurrentLimitEnable(true)); - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - cfg.refresh(configuration.ClosedLoopRamps); - cfg.apply(configuration.ClosedLoopRamps.withVoltageClosedLoopRampPeriod(rampRate)); - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - @Override - public DCMotor getSimMotor() { - if (simMotor == null) { - simMotor = DCMotor.getKrakenX60(1); - } - return simMotor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder; - } - - /** Closes handles for unit testing. */ - @Override - public void close() { - motor.close(); - } -} diff --git a/src/main/java/swervelib/motors/TalonFXSwerve.java b/src/main/java/swervelib/motors/TalonFXSwerve.java deleted file mode 100644 index e153c760..00000000 --- a/src/main/java/swervelib/motors/TalonFXSwerve.java +++ /dev/null @@ -1,402 +0,0 @@ -package swervelib.motors; - -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Rotations; -import static edu.wpi.first.units.Units.Volts; - -import com.ctre.phoenix6.configs.TalonFXConfiguration; -import com.ctre.phoenix6.configs.TalonFXConfigurator; -import com.ctre.phoenix6.controls.MotionMagicVoltage; -import com.ctre.phoenix6.controls.VelocityVoltage; -import com.ctre.phoenix6.hardware.TalonFX; -import com.ctre.phoenix6.signals.FeedbackSensorSourceValue; -import com.ctre.phoenix6.signals.InvertedValue; -import com.ctre.phoenix6.signals.NeutralModeValue; -import edu.wpi.first.math.system.plant.DCMotor; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** {@link com.ctre.phoenix6.hardware.TalonFX} Swerve Motor. Made by Team 1466 WebbRobotics. */ -public class TalonFXSwerve extends SwerveMotor { - - /** Wait time for status frames to show up. */ - public static double STATUS_TIMEOUT_SECONDS = 0.02; - /** Factory default already occurred. */ - private final boolean factoryDefaultOccurred = false; - /** Whether the absolute encoder is integrated. */ - private final boolean absoluteEncoder = false; - /** Motion magic angle voltage setter. */ - private final MotionMagicVoltage m_angleVoltageSetter = new MotionMagicVoltage(0); - /** Velocity voltage setter for controlling drive motor. */ - private final VelocityVoltage m_velocityVoltageSetter = new VelocityVoltage(0); - /** TalonFX motor controller. */ - private final TalonFX motor; - /** Conversion factor for the motor. */ - private double conversionFactor; - /** Current TalonFX configuration. */ - private TalonFXConfiguration configuration = new TalonFXConfiguration(); - /** Current TalonFX Configurator. */ - private TalonFXConfigurator cfg; - - /** - * Constructor for TalonFX swerve motor. - * - * @param motor Motor to use. - * @param isDriveMotor Whether this motor is a drive motor. - * @param motorType {@link DCMotor} which the {@link TalonFX} is attached to. - */ - public TalonFXSwerve(TalonFX motor, boolean isDriveMotor, DCMotor motorType) { - this.isDriveMotor = isDriveMotor; - this.motor = motor; - this.cfg = motor.getConfigurator(); - this.simMotor = motorType; - - factoryDefaults(); - clearStickyFaults(); - - // if (SwerveDriveTelemetry.isSimulation) - // { - //// PhysicsSim.getInstance().addTalonFX(motor, .25, 6800); - // } - } - - /** - * Construct the TalonFX swerve motor given the ID and CANBus. - * - * @param id ID of the TalonFX on the CANBus. - * @param canbus CANBus on which the TalonFX is on. - * @param isDriveMotor Whether the motor is a drive or steering motor. - * @param motorType {@link DCMotor} which the {@link TalonFX} is attached to. - */ - public TalonFXSwerve(int id, String canbus, boolean isDriveMotor, DCMotor motorType) { - this(new TalonFX(id, canbus), isDriveMotor, motorType); - } - - /** - * Construct the TalonFX swerve motor given the ID. - * - * @param id ID of the TalonFX on the canbus. - * @param isDriveMotor Whether the motor is a drive or steering motor. - * @param motorType {@link DCMotor} which the {@link TalonFX} is attached to. - */ - public TalonFXSwerve(int id, boolean isDriveMotor, DCMotor motorType) { - this(new TalonFX(id), isDriveMotor, motorType); - } - - /** Configure the factory defaults. */ - @Override - public void factoryDefaults() { - if (!factoryDefaultOccurred) { - configuration.MotorOutput.NeutralMode = NeutralModeValue.Brake; - configuration.ClosedLoopGeneral.ContinuousWrap = true; - cfg.apply(configuration); - - m_angleVoltageSetter.UpdateFreqHz = 0; - // m_angleVoltageExpoSetter.UpdateFreqHz = 0; - m_velocityVoltageSetter.UpdateFreqHz = 0; - // motor.configFactoryDefault(); - // motor.setSensorPhase(true); - // motor.configSelectedFeedbackSensor(TalonFXFeedbackDevice.IntegratedSensor, 0, 30); - // motor.configNeutralDeadband(0.001); - } - } - - @Override - public void close() { - motor.close(); - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - motor.clearStickyFaults(); - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - // Do not support. - return this; - } - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply for position. - *


- * Degrees:
- * - * 360 / (angleGearRatio * encoderTicksPerRotation) - *
- *


- * Meters:
- * - * (Math.PI * wheelDiameter) / (driveGearRatio * encoderTicksPerRotation) - * - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - cfg.refresh(configuration); - - positionConversionFactor = 1 / positionConversionFactor; - if (!isDriveMotor) { - positionConversionFactor *= 360; - } - conversionFactor = positionConversionFactor; - - configuration.MotionMagic = - configuration.MotionMagic.withMotionMagicCruiseVelocity(100.0 / positionConversionFactor) - .withMotionMagicAcceleration((100.0 / positionConversionFactor) / 0.100) - .withMotionMagicExpo_kV(0.12 * positionConversionFactor) - .withMotionMagicExpo_kA(0.1); - - configuration.Feedback.withFeedbackSensorSource(FeedbackSensorSourceValue.RotorSensor) - .withSensorToMechanismRatio(positionConversionFactor); - - cfg.apply(configuration); - } - - /** - * Configure the PIDF values for the closed loop controller. 0 is disabled or off. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - - cfg.refresh(configuration.Slot0); - cfg.apply( - configuration.Slot0.withKP(config.p).withKI(config.i).withKD(config.d).withKS(config.f)); - // configuration.slot0.integralZone = config.iz; - // configuration.slot0.closedLoopPeakOutput = config.output.max; - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - cfg.refresh(configuration.ClosedLoopGeneral); - configuration.ClosedLoopGeneral.ContinuousWrap = true; - cfg.apply(configuration.ClosedLoopGeneral); - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - cfg.refresh(configuration.ClosedLoopGeneral); - configuration.ClosedLoopGeneral.ContinuousWrap = false; - cfg.apply(configuration.ClosedLoopGeneral); - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - motor.setNeutralMode(isBrakeMode ? NeutralModeValue.Brake : NeutralModeValue.Coast); - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - // Timer.delay(1); - cfg.refresh(configuration.MotorOutput); - configuration.MotorOutput.withInverted( - inverted ? InvertedValue.CounterClockwise_Positive : InvertedValue.Clockwise_Positive); - cfg.apply(configuration.MotorOutput); - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - // Do nothing - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.set(percentOutput); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - setReference(setpoint, feedforward, getPosition()); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - // if (SwerveDriveTelemetry.isSimulation) - // { - // PhysicsSim.getInstance().run(); - // } - - if (isDriveMotor) { - motor.setControl(m_velocityVoltageSetter.withVelocity(setpoint).withFeedForward(feedforward)); - } else { - motor.setControl(m_angleVoltageSetter.withPosition(setpoint / 360.0)); - } - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getMotorVoltage().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue().in(Volts); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - @Override - public double getAppliedOutput() { - return motor.getDutyCycle().waitForUpdate(STATUS_TIMEOUT_SECONDS).getValue(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity in Meters Per Second, or Degrees per Second. - */ - @Override - public double getVelocity() { - return motor.getVelocity().getValue().magnitude(); - } - - /** - * Get the position of the integrated encoder. - * - * @return Position in Meters or Degrees. - */ - @Override - public double getPosition() { - return motor.getPosition().getValue().magnitude(); - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. Should be angle in degrees or meters. - */ - @Override - public void setPosition(double position) { - if (!absoluteEncoder && !SwerveDriveTelemetry.isSimulation) { - cfg.setPosition(Degrees.of(position).in(Rotations)); - } - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - // Do not implement - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - cfg.refresh(configuration.CurrentLimits); - cfg.apply( - configuration.CurrentLimits.withSupplyCurrentLimit(currentLimit) - .withSupplyCurrentLimitEnable(true)); - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - cfg.refresh(configuration.ClosedLoopRamps); - cfg.apply(configuration.ClosedLoopRamps.withVoltageClosedLoopRampPeriod(rampRate)); - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - @Override - public DCMotor getSimMotor() { - if (simMotor == null) { - simMotor = DCMotor.getKrakenX60(1); - } - return simMotor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder; - } -} diff --git a/src/main/java/swervelib/motors/TalonSRXSwerve.java b/src/main/java/swervelib/motors/TalonSRXSwerve.java deleted file mode 100644 index 92fac1f7..00000000 --- a/src/main/java/swervelib/motors/TalonSRXSwerve.java +++ /dev/null @@ -1,440 +0,0 @@ -package swervelib.motors; - -import com.ctre.phoenix.motorcontrol.ControlMode; -import com.ctre.phoenix.motorcontrol.DemandType; -import com.ctre.phoenix.motorcontrol.FeedbackDevice; -import com.ctre.phoenix.motorcontrol.NeutralMode; -import com.ctre.phoenix.motorcontrol.StatusFrameEnhanced; -import com.ctre.phoenix.motorcontrol.can.TalonSRXConfiguration; -import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX; -import edu.wpi.first.math.system.plant.DCMotor; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.encoders.TalonSRXEncoderSwerve; -import swervelib.math.SwerveMath; -import swervelib.parser.PIDFConfig; -import swervelib.parser.json.modules.ConversionFactorsJson; -import swervelib.telemetry.SwerveDriveTelemetry; - -/** {@link com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX} Swerve Motor. */ -public class TalonSRXSwerve extends SwerveMotor { - - /** Factory default already occurred. */ - private final boolean factoryDefaultOccurred = false; - /** Current TalonSRX configuration. */ - private final TalonSRXConfiguration configuration = new TalonSRXConfiguration(); - /** Whether the absolute encoder is integrated. */ - private boolean absoluteEncoder = false; - /** TalonSRX motor controller. */ - private final WPI_TalonSRX motor; - /** - * The position conversion factor to convert raw sensor units to Meters Per 100ms, or Ticks to - * Degrees. - */ - private double positionConversionFactor = 1; - /** Module Conversion factors to use. */ - private ConversionFactorsJson moduleConversionFactors; - /** If the TalonSRX configuration has changed. */ - private boolean configChanged = true; - /** Nominal voltage default to use with feedforward. */ - private double nominalVoltage = 12.0; - - /** - * Constructor for TalonSRX swerve motor. - * - * @param motor Motor to use. - * @param isDriveMotor Whether this motor is a drive motor. - * @param motorType {@link DCMotor} which the {@link WPI_TalonSRX} is attached to. - */ - public TalonSRXSwerve(WPI_TalonSRX motor, boolean isDriveMotor, DCMotor motorType) { - this.isDriveMotor = isDriveMotor; - this.motor = motor; - this.simMotor = motorType; - motor.configSelectedFeedbackSensor(FeedbackDevice.QuadEncoder); - - factoryDefaults(); - clearStickyFaults(); - } - - /** - * Construct the TalonSRX swerve motor given the ID. - * - * @param id ID of the TalonSRX on the canbus. - * @param isDriveMotor Whether the motor is a drive or steering motor. - * @param motorType {@link DCMotor} which the {@link WPI_TalonSRX} is attached to. - */ - public TalonSRXSwerve(int id, boolean isDriveMotor, DCMotor motorType) { - this(new WPI_TalonSRX(id), isDriveMotor, motorType); - } - - @Override - public void close() { - motor.close(); - } - - /** Configure the factory defaults. */ - @Override - public void factoryDefaults() { - if (!factoryDefaultOccurred) { - motor.configFactoryDefault(); - motor.setSensorPhase(true); - } - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - motor.clearStickyFaults(); - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - absoluteEncoder = encoder instanceof TalonSRXEncoderSwerve; - // Do not support. - return this; - } - - /** - * Configure the integrated encoder for the swerve module. Sets the conversion factors for - * position and velocity. - * - * @param positionConversionFactor The conversion factor to apply for position. - *


- * Degrees:
- * - * 360 / (angleGearRatio * encoderTicksPerRotation) - *
- *


- * Meters:
- * - * (Math.PI * wheelDiameter) / (driveGearRatio * encoderTicksPerRotation) - * - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - this.positionConversionFactor = positionConversionFactor; - // Taken from democat's library. - // https://github.com/democat3457/swerve-lib/blob/7c03126b8c22f23a501b2c2742f9d173a5bcbc40/src/main/java/com/swervedrivespecialties/swervelib/ctre/Falcon500DriveControllerFactoryBuilder.java#L16 - configureCANStatusFrames(250); - } - - /** - * Set the CAN status frames. - * - * @param CANStatus1 Applied Motor Output, Fault Information, Limit Switch Information - */ - public void configureCANStatusFrames(int CANStatus1) { - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_1_General, CANStatus1); - } - - /** - * Set the CAN status frames. - * - * @param CANStatus1 Applied Motor Output, Fault Information, Limit Switch Information - * @param CANStatus2 Selected Sensor Position (PID 0), Selected Sensor Velocity (PID 0), Brushed - * Supply Current Measurement, Sticky Fault Information - * @param CANStatus3 Quadrature Information - * @param CANStatus4 Analog Input, Supply Battery Voltage, Controller Temperature - * @param CANStatus8 Pulse Width Information - * @param CANStatus10 Motion Profiling/Motion Magic Information - * @param CANStatus12 Selected Sensor Position (Aux PID 1), Selected Sensor Velocity (Aux PID 1) - * @param CANStatus13 PID0 (Primary PID) Information - * @param CANStatus14 PID1 (Auxiliary PID) Information - * @param CANStatus21 Integrated Sensor Position (Talon FX), Integrated Sensor Velocity (Talon FX) - * @param CANStatusCurrent Brushless Supply Current Measurement, Brushless Stator Current - * Measurement - */ - public void configureCANStatusFrames( - int CANStatus1, - int CANStatus2, - int CANStatus3, - int CANStatus4, - int CANStatus8, - int CANStatus10, - int CANStatus12, - int CANStatus13, - int CANStatus14, - int CANStatus21, - int CANStatusCurrent) { - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_1_General, CANStatus1); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_2_Feedback0, CANStatus2); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_3_Quadrature, CANStatus3); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_4_AinTempVbat, CANStatus4); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_8_PulseWidth, CANStatus8); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_10_Targets, CANStatus10); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_12_Feedback1, CANStatus12); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_13_Base_PIDF0, CANStatus13); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_14_Turn_PIDF1, CANStatus14); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_21_FeedbackIntegrated, CANStatus21); - motor.setStatusFramePeriod(StatusFrameEnhanced.Status_Brushless_Current, CANStatusCurrent); - - // TODO: Configure Status Frame 2 thru 21 if necessary - // https://v5.docs.ctr-electronics.com/en/stable/ch18_CommonAPI.html#setting-status-frame-periods - } - - /** - * Configure the PIDF values for the closed loop controller. 0 is disabled or off. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - configuration.slot0.kP = config.p; - configuration.slot0.kI = config.i; - configuration.slot0.kD = config.d; - configuration.slot0.kF = config.f; - configuration.slot0.integralZone = config.iz; - configuration.slot0.closedLoopPeakOutput = config.output.max; - configChanged = true; - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - // Do nothing - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - // Do nothing - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - motor.setNeutralMode(isBrakeMode ? NeutralMode.Brake : NeutralMode.Coast); - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - motor.setInverted(inverted); - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - if (configChanged) { - motor.configAllSettings(configuration, 250); - configChanged = false; - } - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.set(percentOutput); - } - - /** - * Convert the setpoint into native sensor units. - * - * @param setpoint Setpoint to mutate. In meters per second or degrees. - * @param position Position in degrees, only used on angle motors. - * @return Setpoint as native sensor units. Encoder ticks per 100ms, or Encoder tick. - */ - public double convertToNativeSensorUnits(double setpoint, double position) { - setpoint = - isDriveMotor ? setpoint * .1 : SwerveMath.placeInAppropriate0To360Scope(position, setpoint); - return setpoint / positionConversionFactor; - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - setReference(setpoint, feedforward, getPosition()); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - - burnFlash(); - - motor.set( - isDriveMotor ? ControlMode.Velocity : ControlMode.Position, - convertToNativeSensorUnits(setpoint, position), - DemandType.ArbitraryFeedForward, - feedforward / nominalVoltage); - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getMotorOutputVoltage(); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the applied dutycycle output. - * - * @return Applied dutycycle output to the motor. - */ - @Override - public double getAppliedOutput() { - return motor.getMotorOutputPercent(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity in Meters Per Second, or Degrees per Second. - */ - @Override - public double getVelocity() { - return (motor.getSelectedSensorVelocity() * 10) * positionConversionFactor; - } - - /** - * Get the position of the integrated encoder. - * - * @return Position in Meters or Degrees. - */ - @Override - public double getPosition() { - if (isDriveMotor) { - return motor.getSelectedSensorPosition() * positionConversionFactor; - } else { - var pos = motor.getSelectedSensorPosition() * positionConversionFactor; - return pos; - } - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. Should be angle in degrees or meters. - */ - @Override - public void setPosition(double position) { - if (!absoluteEncoder && !SwerveDriveTelemetry.isSimulation) { - motor.setSelectedSensorPosition(position / positionConversionFactor, 0, 0); - } - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - configuration.voltageCompSaturation = nominalVoltage; - configChanged = true; - this.nominalVoltage = nominalVoltage; - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - configuration.continuousCurrentLimit = currentLimit; - configuration.peakCurrentLimit = currentLimit; - configChanged = true; - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - configuration.closedloopRamp = rampRate; - configuration.openloopRamp = rampRate; - configChanged = true; - } - - /** - * Set the selected feedback device for the TalonSRX. - * - * @param feedbackDevice Feedback device to select. - */ - public void setSelectedFeedbackDevice(FeedbackDevice feedbackDevice) { - configuration.primaryPID.selectedFeedbackSensor = feedbackDevice; - configChanged = true; - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Get the {@link DCMotor} of the motor class. - * - * @return {@link DCMotor} of this type. - */ - @Override - public DCMotor getSimMotor() { - if (simMotor == null) { - simMotor = DCMotor.getCIM(1); - } - return simMotor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder; - } -} diff --git a/src/main/java/swervelib/motors/ThriftyNovaSwerve.java b/src/main/java/swervelib/motors/ThriftyNovaSwerve.java deleted file mode 100644 index c4218b5d..00000000 --- a/src/main/java/swervelib/motors/ThriftyNovaSwerve.java +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package swervelib.motors; - -import com.thethriftybot.Conversion; -import com.thethriftybot.Conversion.PositionUnit; -import com.thethriftybot.Conversion.VelocityUnit; -import com.thethriftybot.ThriftyNova; -import com.thethriftybot.ThriftyNova.CurrentType; -import com.thethriftybot.ThriftyNova.EncoderType; -import com.thethriftybot.ThriftyNova.PIDSlot; -import edu.wpi.first.math.controller.PIDController; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.wpilibj.DataLogManager; -import edu.wpi.first.wpilibj.RobotBase; -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.parser.PIDFConfig; -import swervelib.telemetry.SwerveDriveTelemetry; -import swervelib.telemetry.SwerveDriveTelemetry.TelemetryVerbosity; - -/** An implementation of {@link ThriftyNova} as a {@link SwerveMotor}. */ -public class ThriftyNovaSwerve extends SwerveMotor { - - /** {@link DCMotor} for simulation and calculations. */ - private final DCMotor simMotor; - /** Closed-loop PID controller. */ - public PIDController pid; - /** ThriftyNova Instance. */ - private ThriftyNova motor; - /** Factory default already occurred. */ - private boolean factoryDefaultOccurred = false; - /** Position conversion object for the motor encoder */ - private Conversion positionConversion; - /** Velocity conversion object for the motor encoder */ - private Conversion velocityConversion; - /** The position conversion factor for the encoder */ - private double positionConversionFactor = 1.0; - /** The position conversion factor for the encoder */ - private double velocityConversionFactor = 1.0 / 60.0; - /** Absolute encoder attached to the SparkMax (if exists) */ - private Optional absoluteEncoder = Optional.empty(); - /** Supplier for the velocity of the motor controller. */ - private Supplier velocity; - /** Supplier for the position of the motor controller. */ - private Supplier position; - - /** - * Initialize the swerve motor. - * - * @param motor The SwerveMotor as a ThriftyNova object. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param motorType {@link DCMotor} controlled by the {@link ThriftyNova} - */ - public ThriftyNovaSwerve(ThriftyNova motor, boolean isDriveMotor, DCMotor motorType) { - this.motor = motor; - this.isDriveMotor = isDriveMotor; - this.simMotor = motorType; - factoryDefaults(); - clearStickyFaults(); - - motor.usePIDSlot(PIDSlot.SLOT0); - pid = new PIDController(0, 0, 0); - motor.pid0.setPID(pid); - - if (isDriveMotor) { - positionConversion = new Conversion(PositionUnit.ROTATIONS, EncoderType.INTERNAL); - velocityConversion = new Conversion(VelocityUnit.ROTATIONS_PER_SEC, EncoderType.INTERNAL); - } else { - positionConversion = new Conversion(PositionUnit.ROTATIONS, EncoderType.INTERNAL); - velocityConversion = new Conversion(VelocityUnit.ROTATIONS_PER_SEC, EncoderType.INTERNAL); - } - - position = this::getConvertedPosition; - velocity = this::getConvertedVelocity; - } - - /** - * Initialize the {@link SwerveMotor} as a {@link ThriftyNova} connected to a Brushless Motor. - * - * @param id CAN ID of the ThriftyNova. - * @param isDriveMotor Is the motor being initialized a drive motor? - * @param motor {@link DCMotor} controlled by the {@link ThriftyNova} - */ - public ThriftyNovaSwerve(int id, boolean isDriveMotor, DCMotor motor) { - this(new ThriftyNova(id), isDriveMotor, motor); - } - - /** - * Close the motor controller and release any resources it may have acquired. - * - * @throws RuntimeException If an exception is thrown while closing the motor. - */ - @Override - public void close() { - try { - motor.close(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private double getConvertedPosition() { - double motorPosition = motor.getPosition(); - double convertedPosition = motorPosition / positionConversionFactor; - return convertedPosition; - } - - /** - * Get the velocity of the motor in meters per second or degrees per second after conversion. - * - * @return velocity in meters per second or degrees per second. - */ - private double getConvertedVelocity() { - double motorVelocity = motor.getVelocity(); - double convertedVelocity = motorVelocity / velocityConversionFactor; - - return convertedVelocity; - } - - /** Set factory defaults on the motor controller. */ - @Override - public void factoryDefaults() { - // Factory defaults from - // https://docs.thethriftybot.com/thrifty-nova/gqCPUYXcVoOZ4KW3DqIr/software-resources/configure-controller-settings/factory-default - if (!factoryDefaultOccurred) { - if (RobotBase.isReal()) { - motor.factoryReset(); - } - factoryDefaultOccurred = true; - } - } - - /** Clear the sticky faults on the motor controller. */ - @Override - public void clearStickyFaults() { - if (RobotBase.isReal()) { - motor.clearErrors(); - } - } - - /** - * Set the absolute encoder to be a compatible absolute encoder. - * - * @param encoder The encoder to use. - * @return The {@link SwerveMotor} for easy instantiation. - */ - @Override - public SwerveMotor setAbsoluteEncoder(SwerveAbsoluteEncoder encoder) { - if (encoder == null) { - motor.useEncoderType(EncoderType.INTERNAL); - position = this::getConvertedPosition; - velocity = this::getConvertedVelocity; - } else { - absoluteEncoder = Optional.of(encoder); - position = absoluteEncoder.get()::getAbsolutePosition; - velocity = absoluteEncoder.get()::getVelocity; - motor.useEncoderType(EncoderType.ABS); - } - return this; - } - - /** - * Configure the integrated encoder for the ThriftyNova swerve module. Sets the conversion factors - * for position and velocity, and configures the CAN status frames. - * - * @param positionConversionFactor The conversion factor to apply for position. - */ - @Override - public void configureIntegratedEncoder(double positionConversionFactor) { - this.positionConversionFactor = positionConversionFactor; - this.velocityConversionFactor = positionConversionFactor; - - motor.useEncoderType(EncoderType.INTERNAL); - configureCANStatusFrames(0.25, 0.01, 0.01, 0.02, 0.20); - } - - /** - * Set the CAN status frames. - * - * @param fault Fault transmission rate - * @param sensor Sensor transmission rate - * @param quadSensor External quad encoder transmission rate - * @param control Control frame transmission rate - * @param current Current feedback transmission rate - */ - public void configureCANStatusFrames( - double fault, double sensor, double quadSensor, double control, double current) { - if (RobotBase.isReal()) { - motor.canFreq.setFault(fault); - motor.canFreq.setSensor(sensor); - motor.canFreq.setQuadSensor(quadSensor); - motor.canFreq.setControl(control); - motor.canFreq.setCurrent(current); - checkErrors("Configuring CAN status frames failed: "); - } - } - - /** - * Configure the PIDF values for the closed loop controller. 0 is disabled or off. - * - * @param config Configuration class holding the PIDF values. - */ - @Override - public void configurePIDF(PIDFConfig config) { - if (RobotBase.isReal()) { - motor.pid0.setP(config.p).setI(config.i).setD(config.d); - motor.usePIDSlot(PIDSlot.SLOT0); - checkErrors("Configuring PIDF failed: "); - } - } - - /** - * Configure the PID wrapping for the position closed loop controller. - * - * @param minInput Minimum PID input. - * @param maxInput Maximum PID input. - */ - @Override - public void configurePIDWrapping(double minInput, double maxInput) { - // Do nothing - } - - /** Disable PID Wrapping on the motor. */ - @Override - public void disablePIDWrapping() { - // Do nothing - } - - /** - * Set the idle mode. - * - * @param isBrakeMode Set the brake mode. - */ - @Override - public void setMotorBrake(boolean isBrakeMode) { - if (RobotBase.isReal()) { - motor.setBrakeMode(isBrakeMode); - checkErrors("Setting motor brake mode failed: "); - } - } - - /** - * Set the motor to be inverted. - * - * @param inverted State of inversion. - */ - @Override - public void setInverted(boolean inverted) { - if (RobotBase.isReal()) { - motor.setInverted(inverted); - checkErrors("Setting motor inversion failed: "); - } - } - - /** Save the configurations from flash to EEPROM. */ - @Override - public void burnFlash() { - // Do nothing - } - - /** - * Set the percentage output. - * - * @param percentOutput percent out for the motor controller. - */ - @Override - public void set(double percentOutput) { - motor.setPercent(percentOutput); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in MPS or Angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - */ - @Override - public void setReference(double setpoint, double feedforward) { - setReference(setpoint, feedforward, getPosition()); - } - - /** - * Set the closed loop PID controller reference point. - * - * @param setpoint Setpoint in meters per second or angle in degrees. - * @param feedforward Feedforward in volt-meter-per-second or kV. - * @param position Only used on the angle motor, the position of the motor in degrees. - */ - @Override - public void setReference(double setpoint, double feedforward, double position) { - if (RobotBase.isReal()) { - if (isDriveMotor) { - double convertedSetpoint = setpoint / velocityConversionFactor; - double motorSetpoint = velocityConversion.toMotor(convertedSetpoint); - motor.setVelocity(motorSetpoint, feedforward); - } else { - double convertedSetpoint = - absoluteEncoder.map(it -> setpoint).orElse(setpoint / positionConversionFactor); - double motorSetpoint = positionConversion.toMotor(convertedSetpoint); - motor.setPosition(motorSetpoint); - } - } - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getVoltage() { - return motor.getVoltage(); - } - - /** - * Set the voltage of the motor. - * - * @param voltage Voltage to set. - */ - @Override - public void setVoltage(double voltage) { - motor.setVoltage(voltage); - } - - /** - * Get the voltage output of the motor controller. - * - * @return Voltage output. - */ - @Override - public double getAppliedOutput() { - return motor.getStatorCurrent(); - } - - /** - * Get the velocity of the integrated encoder. - * - * @return velocity in Meters Per Second, or Degrees per Second. - */ - @Override - public double getVelocity() { - return velocity.get(); - } - - /** - * Get the position of the integrated encoder. - * - * @return Position in Meters or Degrees. - */ - @Override - public double getPosition() { - return position.get(); - } - - /** - * Set the integrated encoder position. - * - * @param position Integrated encoder position. Should be angle in degrees or meters. - */ - @Override - public void setPosition(double position) { - if (!absoluteEncoder.isPresent()) { - double convertedPosition = position / positionConversionFactor; - double motorPosition = positionConversion.toMotor(convertedPosition); - motor.setEncoderPosition(motorPosition); - } - } - - /** - * Set the voltage compensation for the swerve module motor. - * - * @param nominalVoltage Nominal voltage for operation to output to. - */ - @Override - public void setVoltageCompensation(double nominalVoltage) { - motor.setVoltageCompensation(nominalVoltage); - } - - /** - * Set the current limit for the swerve drive motor, remember this may cause jumping if used in - * conjunction with voltage compensation. This is useful to protect the motor from current spikes. - * - * @param currentLimit Current limit in AMPS at free speed. - */ - @Override - public void setCurrentLimit(int currentLimit) { - if (RobotBase.isReal()) { - motor.setMaxCurrent(CurrentType.STATOR, currentLimit); - checkErrors("Setting current limit failed: "); - } - } - - /** - * Set the maximum rate the open/closed loop output can change by. - * - * @param rampRate Time in seconds to go from 0 to full throttle. - */ - @Override - public void setLoopRampRate(double rampRate) { - if (RobotBase.isReal()) { - motor.setRampUp(rampRate); - motor.setRampDown(rampRate); - checkErrors("Setting loop ramp rate failed: "); - } - } - - /** - * Get the motor object from the module. - * - * @return Motor object. - */ - @Override - public Object getMotor() { - return motor; - } - - /** - * Queries whether the absolute encoder is directly attached to the motor controller. - * - * @return connected absolute encoder state. - */ - @Override - public boolean usingExternalFeedbackSensor() { - return absoluteEncoder.isPresent(); - } - - /** - * Checks for errors in the motor and logs them if any are found. - * - * @param message the message to prepend to the log and print statement - */ - private void checkErrors(String message) { - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.HIGH.ordinal()) { - List errors = motor.getErrors(); - if (errors.size() > 0) { - for (ThriftyNova.Error error : errors) { - if (SwerveDriveTelemetry.verbosity.ordinal() >= TelemetryVerbosity.MACHINE.ordinal()) { - System.out.println(this.getClass().getSimpleName() + ": " + message + error.toString()); - } - DataLogManager.log(this.getClass().getSimpleName() + ": " + message + error.toString()); - } - } - motor.clearErrors(); - } - } - - /** - * Get the simulated {@link DCMotor} associated with this motor controller. - * - * @return Simulated {@link DCMotor} instance. - */ - @Override - public DCMotor getSimMotor() { - return simMotor; - } -} diff --git a/src/main/java/swervelib/motors/package-info.java b/src/main/java/swervelib/motors/package-info.java deleted file mode 100644 index de65fb08..00000000 --- a/src/main/java/swervelib/motors/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Swerve motor controller wrappers which implement {@link swervelib.motors.SwerveMotor}. */ -package swervelib.motors; diff --git a/src/main/java/swervelib/package-info.java b/src/main/java/swervelib/package-info.java deleted file mode 100644 index a9884ca3..00000000 --- a/src/main/java/swervelib/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Yet-Another Generic Swerve Library (YAGSL) main package AKA swervelib. - * - * @version 1.0.0 - */ -package swervelib; diff --git a/src/main/java/swervelib/parser/Cache.java b/src/main/java/swervelib/parser/Cache.java deleted file mode 100644 index debc91cb..00000000 --- a/src/main/java/swervelib/parser/Cache.java +++ /dev/null @@ -1,87 +0,0 @@ -package swervelib.parser; - -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj.RobotController; -import java.util.function.Supplier; - -/** Cache for frequently requested data. */ -public class Cache { - - /** Cached value. */ - private T value; - /** Supplier for cached value. */ - private Supplier supplier; - /** Timestamp in microseconds. */ - private long timestamp; - /** Validity period in microseconds. */ - private long validityPeriod; - - /** - * Cache for arbitrary values. - * - * @param val Value to cache. - * @param validityPeriod Validity period in milliseconds. - */ - public Cache(Supplier val, long validityPeriod) { - supplier = val; - value = supplier.get(); - timestamp = RobotController.getFPGATime(); - this.validityPeriod = validityPeriod * 1000L; - } - - /** - * Return whether the cache is stale. - * - * @return The stale state of the cache. - */ - public boolean isStale() { - return (RobotController.getFPGATime() - timestamp) > validityPeriod; - } - - /** - * Update the cache value and timestamp. - * - * @return {@link Cache} used. - */ - public Cache update() { - this.value = supplier.get(); - this.timestamp = RobotController.getFPGATime(); - return this; - } - - /** - * Update the supplier to a new source. Updates the value and timestamp as well. - * - * @param supplier new supplier source. - * @return {@link Cache} for chaining. - */ - public Cache updateSupplier(Supplier supplier) { - this.supplier = supplier; - update(); - return this; - } - - /** - * Update the validity period for the cached value, also updates the value. - * - * @param validityPeriod The new validity period in milliseconds. - * @return {@link Cache} for chaining. - */ - public Cache updateValidityPeriod(long validityPeriod) { - this.validityPeriod = validityPeriod * 1000L; - update(); - return this; - } - - /** - * Get the most up to date cached value. - * - * @return {@link T} updated to the latest cached version. - */ - public T getValue() { - if (isStale() || RobotBase.isSimulation()) { - update(); - } - return value; - } -} diff --git a/src/main/java/swervelib/parser/PIDFConfig.java b/src/main/java/swervelib/parser/PIDFConfig.java deleted file mode 100644 index 5e3f25c6..00000000 --- a/src/main/java/swervelib/parser/PIDFConfig.java +++ /dev/null @@ -1,88 +0,0 @@ -package swervelib.parser; - -import edu.wpi.first.math.controller.PIDController; -import swervelib.parser.deserializer.PIDFRange; - -/** Hold the PIDF and Integral Zone values for a PID. */ -public class PIDFConfig { - - /** Proportional Gain for PID. */ - public double p; - /** Integral Gain for PID. */ - public double i; - /** Derivative Gain for PID. */ - public double d; - /** Feedforward value for PID. */ - public double f; - /** Integral zone of the PID. */ - public double iz; - - /** The PIDF output range. */ - public PIDFRange output = new PIDFRange(); - - /** Used when parsing PIDF values from JSON. */ - public PIDFConfig() {} - - /** - * PIDF Config constructor to contain the values. - * - * @param p P gain. - * @param i I gain. - * @param d D gain. - * @param f F gain. - * @param iz Intergral zone. - */ - public PIDFConfig(double p, double i, double d, double f, double iz) { - this.p = p; - this.i = i; - this.d = d; - this.f = f; - this.iz = iz; - } - - /** - * PIDF Config constructor to contain the values. - * - * @param p P gain. - * @param i I gain. - * @param d D gain. - * @param f F gain. - */ - public PIDFConfig(double p, double i, double d, double f) { - this(p, i, d, f, 0); - } - - /** - * PIDF Config constructor to contain the values. - * - * @param p P gain. - * @param i I gain. - * @param d D gain. - */ - public PIDFConfig(double p, double i, double d) { - this(p, i, d, 0, 0); - } - - /** - * PIDF Config constructor to contain the values. - * - * @param p P gain. - * @param d D gain. - */ - public PIDFConfig(double p, double d) { - this(p, 0, d, 0, 0); - } - - /** - * Create a PIDController from the PID values. - * - * @return PIDController. - */ - public PIDController createPIDController() { - PIDController pidController = new PIDController(p, i, d); - if (iz != 0) { - pidController.setIZone(iz); - } - return pidController; - } -} diff --git a/src/main/java/swervelib/parser/SwerveControllerConfiguration.java b/src/main/java/swervelib/parser/SwerveControllerConfiguration.java deleted file mode 100644 index 7961032f..00000000 --- a/src/main/java/swervelib/parser/SwerveControllerConfiguration.java +++ /dev/null @@ -1,57 +0,0 @@ -package swervelib.parser; - -import static swervelib.math.SwerveMath.calculateMaxAngularVelocity; - -/** - * Swerve Controller configuration class which is used to configure {@link - * swervelib.SwerveController}. - */ -public class SwerveControllerConfiguration { - - /** PIDF for the heading of the robot. */ - public final PIDFConfig headingPIDF; - /** hypotenuse deadband for the robot angle control joystick. */ - public final double - angleJoyStickRadiusDeadband; // Deadband for the minimum hypot for the heading joystick. - /** Maximum chassis angular velocity in rad/s */ - public double maxAngularVelocity; - - /** - * Construct the swerve controller configuration. Assumes robot is square to fetch maximum angular - * velocity. - * - * @param driveCfg {@link SwerveDriveConfiguration} to fetch the first module X and Y used to - * calculate the maximum angular velocity. - * @param headingPIDF Heading PIDF configuration. - * @param angleJoyStickRadiusDeadband Deadband on radius of angle joystick. - * @param maxSpeedMPS Maximum speed in meters per second for angular velocity, remember if you - * have feet per second use {@link edu.wpi.first.math.util.Units#feetToMeters(double)}. - */ - public SwerveControllerConfiguration( - SwerveDriveConfiguration driveCfg, - PIDFConfig headingPIDF, - double angleJoyStickRadiusDeadband, - double maxSpeedMPS) { - this.maxAngularVelocity = - calculateMaxAngularVelocity( - maxSpeedMPS, - Math.abs(driveCfg.moduleLocationsMeters[0].getX()), - Math.abs(driveCfg.moduleLocationsMeters[0].getY())); - this.headingPIDF = headingPIDF; - this.angleJoyStickRadiusDeadband = angleJoyStickRadiusDeadband; - } - - /** - * Construct the swerve controller configuration. Assumes hypotenuse deadband of 0.5 (minimum - * radius for angle to be set on angle joystick is .5 of the controller). - * - * @param driveCfg Drive configuration. - * @param headingPIDF Heading PIDF configuration. - * @param maxSpeedMPS Maximum speed in meters per second for angular velocity, remember if you - * have feet per second use {@link edu.wpi.first.math.util.Units#feetToMeters(double)}. - */ - public SwerveControllerConfiguration( - SwerveDriveConfiguration driveCfg, PIDFConfig headingPIDF, double maxSpeedMPS) { - this(driveCfg, headingPIDF, 0.5, maxSpeedMPS); - } -} diff --git a/src/main/java/swervelib/parser/SwerveDriveConfiguration.java b/src/main/java/swervelib/parser/SwerveDriveConfiguration.java deleted file mode 100644 index 433f7252..00000000 --- a/src/main/java/swervelib/parser/SwerveDriveConfiguration.java +++ /dev/null @@ -1,139 +0,0 @@ -package swervelib.parser; - -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.system.plant.DCMotor; -import java.util.function.Supplier; -import org.ironmaple.simulation.drivesims.COTS; -import org.ironmaple.simulation.drivesims.GyroSimulation; -import swervelib.SwerveModule; -import swervelib.imu.NavXSwerve; -import swervelib.imu.Pigeon2Swerve; -import swervelib.imu.SwerveIMU; -import swervelib.math.SwerveMath; - -/** Swerve drive configurations used during SwerveDrive construction. */ -public class SwerveDriveConfiguration { - - /** Number of modules on the robot. */ - public final int moduleCount; - /** Swerve Module locations. */ - public Translation2d[] moduleLocationsMeters; - /** Swerve IMU */ - public SwerveIMU imu; - /** Swerve Modules. */ - public SwerveModule[] modules; - /** Physical characteristics of the swerve drive from physicalproperties.json. */ - public SwerveModulePhysicalCharacteristics physicalCharacteristics; - - /** - * Create swerve drive configuration. - * - * @param moduleConfigs Module configuration. - * @param swerveIMU Swerve IMU. - * @param invertedIMU Invert the IMU. - * @param physicalCharacteristics {@link SwerveModulePhysicalCharacteristics} to store in - * association with self. - */ - public SwerveDriveConfiguration( - SwerveModuleConfiguration[] moduleConfigs, - SwerveIMU swerveIMU, - boolean invertedIMU, - SwerveModulePhysicalCharacteristics physicalCharacteristics) { - this.moduleCount = moduleConfigs.length; - this.imu = swerveIMU; - swerveIMU.setInverted(invertedIMU); - this.modules = createModules(moduleConfigs); - this.moduleLocationsMeters = new Translation2d[moduleConfigs.length]; - for (SwerveModule module : modules) { - this.moduleLocationsMeters[module.moduleNumber] = module.configuration.moduleLocation; - } - this.physicalCharacteristics = physicalCharacteristics; - } - - /** - * Create modules based off of the SwerveModuleConfiguration. - * - * @param swerves Swerve constants. - * @return Swerve Modules. - */ - public SwerveModule[] createModules(SwerveModuleConfiguration[] swerves) { - SwerveModule[] modArr = new SwerveModule[swerves.length]; - for (int i = 0; i < swerves.length; i++) { - modArr[i] = new SwerveModule(i, swerves[i]); - } - return modArr; - } - - /** - * Calculate the Drive Base Radius - * - * @return Drive base radius from center of robot to the farthest wheel in meters. - */ - public double getDriveBaseRadiusMeters() { - Translation2d centerOfModules = moduleLocationsMeters[0]; - - // Calculate the Center by adding all module offsets together. - for (int i = 1; i < moduleLocationsMeters.length; i++) { - centerOfModules = centerOfModules.plus(moduleLocationsMeters[i]); - } - - // Return Largest Radius - return centerOfModules.getDistance(moduleLocationsMeters[0]); - } - - /** - * Get the trackwidth of the swerve modules. - * - * @return Effective trackwdtih in Meters - */ - public double getTrackwidth() { - SwerveModuleConfiguration fr = SwerveMath.getSwerveModule(modules, true, false); - SwerveModuleConfiguration fl = SwerveMath.getSwerveModule(modules, true, true); - return fr.moduleLocation.getDistance(fl.moduleLocation); - } - - /** - * Get the tracklength of the swerve modules. - * - * @return Effective tracklength in Meters - */ - public double getTracklength() { - SwerveModuleConfiguration br = SwerveMath.getSwerveModule(modules, false, false); - SwerveModuleConfiguration bl = SwerveMath.getSwerveModule(modules, false, true); - return br.moduleLocation.getDistance(bl.moduleLocation); - } - - /** - * Get the {@link DCMotor} corresponding to the first module's configuration. - * - * @return {@link DCMotor} of the drive motor. - */ - public DCMotor getDriveMotorSim() { - SwerveModuleConfiguration fl = SwerveMath.getSwerveModule(modules, true, true); - return fl.driveMotor.getSimMotor(); - } - - /** - * Get the {@link DCMotor} corresponding to the first module configuration. - * - * @return {@link DCMotor} of the angle motor. - */ - public DCMotor getAngleMotorSim() { - SwerveModuleConfiguration fl = SwerveMath.getSwerveModule(modules, true, true); - return fl.angleMotor.getSimMotor(); - } - - /** - * Get the gyro simulation for the robot. - * - * @return {@link GyroSimulation} gyro simulation. - */ - public Supplier getGyroSim() { - if (imu instanceof Pigeon2Swerve) { - return COTS.ofPigeon2(); - } else if (imu instanceof NavXSwerve) { - return COTS.ofNav2X(); - } - return COTS.ofGenericGyro(); - } -} diff --git a/src/main/java/swervelib/parser/SwerveModuleConfiguration.java b/src/main/java/swervelib/parser/SwerveModuleConfiguration.java deleted file mode 100644 index 1fa3f4ef..00000000 --- a/src/main/java/swervelib/parser/SwerveModuleConfiguration.java +++ /dev/null @@ -1,141 +0,0 @@ -package swervelib.parser; - -import edu.wpi.first.math.geometry.Translation2d; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.motors.SwerveMotor; -import swervelib.parser.json.modules.ConversionFactorsJson; - -/** Swerve Module configuration class which is used to configure {@link swervelib.SwerveModule}. */ -public class SwerveModuleConfiguration { - - /** - * Conversion factor for drive motor onboard PID's and angle PID's. Use {@link - * swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and {@link - * swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)} respectively to - * calculate the conversion factors. - */ - public final ConversionFactorsJson conversionFactors; - /** Angle offset in degrees for the Swerve Module. */ - public final double angleOffset; - /** Whether the absolute encoder is inverted. */ - public final boolean absoluteEncoderInverted; - /** State of inversion of the drive motor. */ - public final boolean driveMotorInverted; - /** State of inversion of the angle motor. */ - public final boolean angleMotorInverted; - /** PIDF configuration options for the angle motor closed-loop PID controller. */ - public PIDFConfig anglePIDF; - /** PIDF configuration options for the drive motor closed-loop PID controller. */ - public PIDFConfig velocityPIDF; - /** Swerve module location relative to the robot. */ - public Translation2d moduleLocation; - /** Physical characteristics of the swerve module. */ - public SwerveModulePhysicalCharacteristics physicalCharacteristics; - /** The drive motor and angle motor of this swerve module. */ - public SwerveMotor driveMotor, angleMotor; - /** The Absolute Encoder for the swerve module. */ - public SwerveAbsoluteEncoder absoluteEncoder; - /** Name for the swerve module for telemetry. */ - public String name; - /** Should do cosine compensation when not pointing correct direction;. */ - public boolean useCosineCompensator; - - /** - * Construct a configuration object for swerve modules. - * - * @param driveMotor Drive {@link SwerveMotor}. - * @param angleMotor Angle {@link SwerveMotor} - * @param absoluteEncoder Absolute encoder {@link SwerveAbsoluteEncoder}. - * @param angleOffset Absolute angle offset to 0. - * @param absoluteEncoderInverted Absolute encoder inverted. - * @param angleMotorInverted State of inversion of the angle motor. - * @param driveMotorInverted Drive motor inverted. - * @param xMeters Module location in meters from the center horizontally. - * @param yMeters Module location in meters from center vertically. - * @param anglePIDF Angle PIDF configuration. - * @param velocityPIDF Velocity PIDF configuration. - * @param physicalCharacteristics Physical characteristics of the swerve module. - * @param name The name for the swerve module. - * @param conversionFactors Conversion factors to be applied to the drive and angle motors. - * @param useCosineCompensator Should use cosineCompensation. - */ - public SwerveModuleConfiguration( - SwerveMotor driveMotor, - SwerveMotor angleMotor, - ConversionFactorsJson conversionFactors, - SwerveAbsoluteEncoder absoluteEncoder, - double angleOffset, - double xMeters, - double yMeters, - PIDFConfig anglePIDF, - PIDFConfig velocityPIDF, - SwerveModulePhysicalCharacteristics physicalCharacteristics, - boolean absoluteEncoderInverted, - boolean driveMotorInverted, - boolean angleMotorInverted, - String name, - boolean useCosineCompensator) { - this.driveMotor = driveMotor; - this.angleMotor = angleMotor; - this.conversionFactors = conversionFactors; - this.absoluteEncoder = absoluteEncoder; - this.angleOffset = angleOffset; - this.absoluteEncoderInverted = absoluteEncoderInverted; - this.driveMotorInverted = driveMotorInverted; - this.angleMotorInverted = angleMotorInverted; - this.moduleLocation = new Translation2d(xMeters, yMeters); - this.anglePIDF = anglePIDF; - this.velocityPIDF = velocityPIDF; - this.physicalCharacteristics = physicalCharacteristics; - this.name = name; - this.useCosineCompensator = useCosineCompensator; - } - - /** - * Construct a configuration object for swerve modules. Assumes the absolute encoder and drive - * motor are not inverted. - * - * @param driveMotor Drive {@link SwerveMotor}. - * @param angleMotor Angle {@link SwerveMotor} - * @param conversionFactors Conversion factors for angle/azimuth motors drive factors. - * @param absoluteEncoder Absolute encoder {@link SwerveAbsoluteEncoder}. - * @param angleOffset Absolute angle offset to 0. - * @param xMeters Module location in meters from the center horizontally. - * @param yMeters Module location in meters from center vertically. - * @param anglePIDF Angle PIDF configuration. - * @param velocityPIDF Velocity PIDF configuration. - * @param physicalCharacteristics Physical characteristics of the swerve module. - * @param name Name for the module. - * @param useCosineCompensator Should use cosineCompensation. - */ - public SwerveModuleConfiguration( - SwerveMotor driveMotor, - SwerveMotor angleMotor, - ConversionFactorsJson conversionFactors, - SwerveAbsoluteEncoder absoluteEncoder, - double angleOffset, - double xMeters, - double yMeters, - PIDFConfig anglePIDF, - PIDFConfig velocityPIDF, - SwerveModulePhysicalCharacteristics physicalCharacteristics, - String name, - boolean useCosineCompensator) { - this( - driveMotor, - angleMotor, - conversionFactors, - absoluteEncoder, - angleOffset, - xMeters, - yMeters, - anglePIDF, - velocityPIDF, - physicalCharacteristics, - false, - false, - false, - name, - useCosineCompensator); - } -} diff --git a/src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java b/src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java deleted file mode 100644 index b38cc47b..00000000 --- a/src/main/java/swervelib/parser/SwerveModulePhysicalCharacteristics.java +++ /dev/null @@ -1,112 +0,0 @@ -package swervelib.parser; - -import swervelib.parser.json.modules.ConversionFactorsJson; - -/** Configuration class which stores physical characteristics shared between every swerve module. */ -public class SwerveModulePhysicalCharacteristics { - - /** Current limits for the Swerve Module. */ - public final int driveMotorCurrentLimit, angleMotorCurrentLimit; - /** The time it takes for the motor to go from 0 to full throttle in seconds. */ - public final double driveMotorRampRate, angleMotorRampRate; - /** The minimum voltage to spin the module or wheel. */ - public final double driveFrictionVoltage, angleFrictionVoltage; - /** Wheel grip tape coefficient of friction on carpet, as described by the vendor. */ - public final double wheelGripCoefficientOfFriction; - /** Steer rotational inertia in (KilogramSquareMeters) kg/m_sq. */ - public final double steerRotationalInertia; - /** Robot mass in Kilograms. */ - public final double robotMassKg; - /** The voltage to use for the smart motor voltage compensation. */ - public double optimalVoltage; - /** - * The conversion factors for the drive and angle motors, created by {@link - * swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and {@link - * swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. - */ - public ConversionFactorsJson conversionFactor; - - /** - * Construct the swerve module physical characteristics. - * - * @param conversionFactors The conversion factors for the drive and angle motors, created by - * {@link swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and - * {@link swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. - * @param wheelGripCoefficientOfFriction Wheel grip coefficient of friction on carpet given by - * manufacturer. - * @param optimalVoltage Optimal robot voltage. - * @param driveMotorCurrentLimit Current limit for the drive motor. - * @param angleMotorCurrentLimit Current limit for the angle motor. - * @param driveMotorRampRate The time in seconds to go from 0 to full throttle on the motor. - * (Prevents over drawing power from battery) - * @param angleMotorRampRate The time in seconds to go from 0 to full throttle on the motor. - * (Prevents overdrawing power and power loss). - * @param angleFrictionVoltage Angle motor minimum voltage. - * @param driveFrictionVoltage Drive motor minimum voltage. - * @param steerRotationalInertia Steering rotational inertia in KilogramSquareMeters. - * @param robotMassKg Robot mass in kG. - */ - public SwerveModulePhysicalCharacteristics( - ConversionFactorsJson conversionFactors, - double wheelGripCoefficientOfFriction, - double optimalVoltage, - int driveMotorCurrentLimit, - int angleMotorCurrentLimit, - double driveMotorRampRate, - double angleMotorRampRate, - double driveFrictionVoltage, - double angleFrictionVoltage, - double steerRotationalInertia, - double robotMassKg) { - this.wheelGripCoefficientOfFriction = wheelGripCoefficientOfFriction; - this.optimalVoltage = optimalVoltage; - - this.conversionFactor = conversionFactors; - // Set the conversion factors to null if they are both 0. - if (conversionFactors != null) { - if (conversionFactors.isAngleEmpty() && conversionFactors.isDriveEmpty()) { - this.conversionFactor = null; - } - } - - this.driveMotorCurrentLimit = driveMotorCurrentLimit; - this.angleMotorCurrentLimit = angleMotorCurrentLimit; - this.driveMotorRampRate = driveMotorRampRate; - this.angleMotorRampRate = angleMotorRampRate; - this.driveFrictionVoltage = driveFrictionVoltage; - this.angleFrictionVoltage = angleFrictionVoltage; - this.steerRotationalInertia = steerRotationalInertia; - this.robotMassKg = robotMassKg; - } - - /** - * Construct the swerve module physical characteristics. Assume coefficient of friction is 1.19 - * (taken from blue nitrile on carpet from Studica) and optimal voltage is 12v. Assumes the drive - * motor current limit is 40A, and the angle motor current limit is 20A. - * - * @param conversionFactors The conversion factors for the drive and angle motors, created by - * {@link swervelib.math.SwerveMath#calculateMetersPerRotation(double, double, double)} and - * {@link swervelib.math.SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. - * @param driveMotorRampRate The time in seconds to go from 0 to full throttle on the motor. - * (Prevents over drawing power from battery) - * @param angleMotorRampRate The time in seconds to go from 0 to full throttle on the motor. - * (Prevents overdrawing power and power loss). - */ - public SwerveModulePhysicalCharacteristics( - ConversionFactorsJson conversionFactors, - double driveMotorRampRate, - double angleMotorRampRate) { - this( - conversionFactors, - 1.19, - 12, - 40, - 20, - driveMotorRampRate, - angleMotorRampRate, - 0.2, - 0.3, - 0.03, - 50); - } -} diff --git a/src/main/java/swervelib/parser/SwerveParser.java b/src/main/java/swervelib/parser/SwerveParser.java deleted file mode 100644 index 643cde4c..00000000 --- a/src/main/java/swervelib/parser/SwerveParser.java +++ /dev/null @@ -1,180 +0,0 @@ -package swervelib.parser; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.wpi.first.math.geometry.Pose2d; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import swervelib.SwerveDrive; -import swervelib.SwerveModule; -import swervelib.math.SwerveMath; -import swervelib.parser.json.ControllerPropertiesJson; -import swervelib.parser.json.ModuleJson; -import swervelib.parser.json.PIDFPropertiesJson; -import swervelib.parser.json.PhysicalPropertiesJson; -import swervelib.parser.json.SwerveDriveJson; - -/** Helper class used to parse the JSON directory with specified configuration options. */ -public class SwerveParser { - - /** Module number mapped to the JSON name. */ - private static final HashMap moduleConfigs = new HashMap<>(); - /** Parsed swervedrive.json */ - public static SwerveDriveJson swerveDriveJson; - /** Parsed controllerproperties.json */ - public static ControllerPropertiesJson controllerPropertiesJson; - /** Parsed modules/pidfproperties.json */ - public static PIDFPropertiesJson pidfPropertiesJson; - /** Parsed modules/physicalproperties.json */ - public static PhysicalPropertiesJson physicalPropertiesJson; - /** Array holding the module jsons given in {@link SwerveDriveJson}. */ - public static ModuleJson[] moduleJsons; - - /** - * Construct a swerve parser. Will throw an error if there is a missing file. - * - * @param directory Directory with swerve configurations. - * @throws IOException if a file doesn't exist. - */ - public SwerveParser(File directory) throws IOException { - checkDirectory(directory); - swerveDriveJson = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readValue(new File(directory, "swervedrive.json"), SwerveDriveJson.class); - controllerPropertiesJson = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readValue( - new File(directory, "controllerproperties.json"), ControllerPropertiesJson.class); - pidfPropertiesJson = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readValue( - new File(directory, "modules/pidfproperties.json"), PIDFPropertiesJson.class); - physicalPropertiesJson = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readValue( - new File(directory, "modules/physicalproperties.json"), - PhysicalPropertiesJson.class); - moduleJsons = new ModuleJson[swerveDriveJson.modules.length]; - for (int i = 0; i < moduleJsons.length; i++) { - moduleConfigs.put(swerveDriveJson.modules[i], i); - File moduleFile = new File(directory, "modules/" + swerveDriveJson.modules[i]); - assert moduleFile.exists(); - moduleJsons[i] = - new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readValue(moduleFile, ModuleJson.class); - } - } - - /** - * Get the swerve module by the json name. - * - * @param name JSON name. - * @param driveConfiguration {@link SwerveDriveConfiguration} to pull from. - * @return {@link SwerveModuleConfiguration} based on the file. - */ - public static SwerveModule getModuleConfigurationByName( - String name, SwerveDriveConfiguration driveConfiguration) { - return driveConfiguration.modules[moduleConfigs.get(name + ".json")]; - } - - /** - * Open JSON file. - * - * @param file JSON File to open. - * @return JsonNode of file. - */ - private JsonNode openJson(File file) { - try { - return new ObjectMapper().readTree(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Check directory structure. - * - * @param directory JSON Configuration Directory - */ - private void checkDirectory(File directory) { - assert new File(directory, "swervedrive.json").exists(); - assert new File(directory, "controllerproperties.json").exists(); - assert new File(directory, "modules").exists() && new File(directory, "modules").isDirectory(); - assert new File(directory, "modules/pidfproperties.json").exists(); - assert new File(directory, "modules/physicalproperties.json").exists(); - } - - /** - * Create {@link SwerveDrive} from JSON configuration directory. - * - * @param maxSpeed Maximum speed of the robot in meters per second, used for both angular - * acceleration used in {@link swervelib.SwerveController} and drive feedforward in {@link - * SwerveMath#createDriveFeedforward(double, double, double)}. - * @return {@link SwerveDrive} instance. - */ - public SwerveDrive createSwerveDrive(double maxSpeed) { - return createSwerveDrive(maxSpeed, Pose2d.kZero); - } - - /** - * Create {@link SwerveDrive} from JSON configuration directory. - * - * @param maxSpeed Maximum speed of the robot in meters per second, used for both angular - * acceleration used in {@link swervelib.SwerveController} and drive feedforward in {@link - * SwerveMath#createDriveFeedforward(double, double, double)}. - * @param angleMotorConversionFactor Angle (AKA azimuth) motor conversion factor to convert motor - * controller PID loop units to degrees, usually created using {@link - * SwerveMath#calculateDegreesPerSteeringRotation(double, double)}. - * @param driveMotorConversion Drive motor conversion factor to convert motor controller PID loop - * units to meters per rotation, usually created using {@link - * SwerveMath#calculateMetersPerRotation(double, double, double)}. - * @return {@link SwerveDrive} instance. - */ - public SwerveDrive createSwerveDrive( - double maxSpeed, double angleMotorConversionFactor, double driveMotorConversion) { - physicalPropertiesJson.conversionFactors.angle.factor = angleMotorConversionFactor; - physicalPropertiesJson.conversionFactors.drive.factor = driveMotorConversion; - return createSwerveDrive(maxSpeed, Pose2d.kZero); - } - - /** - * Create {@link SwerveDrive} from JSON configuration directory. - * - * @param maxSpeed Maximum speed of the robot in meters per second for normal+angular acceleration - * in {@link swervelib.SwerveController} of the robot - * @param initialPose {@link Pose2d} initial pose. - * @return {@link SwerveDrive} instance. - */ - public SwerveDrive createSwerveDrive(double maxSpeed, Pose2d initialPose) { - SwerveModuleConfiguration[] moduleConfigurations = - new SwerveModuleConfiguration[moduleJsons.length]; - for (int i = 0; i < moduleConfigurations.length; i++) { - ModuleJson module = moduleJsons[i]; - moduleConfigurations[i] = - module.createModuleConfiguration( - pidfPropertiesJson.angle, - pidfPropertiesJson.drive, - physicalPropertiesJson.createPhysicalProperties(), - swerveDriveJson.modules[i]); - } - SwerveDriveConfiguration swerveDriveConfiguration = - new SwerveDriveConfiguration( - moduleConfigurations, - swerveDriveJson.imu.createIMU(), - swerveDriveJson.invertedIMU, - physicalPropertiesJson.createPhysicalProperties()); - - return new SwerveDrive( - swerveDriveConfiguration, - controllerPropertiesJson.createControllerConfiguration(swerveDriveConfiguration, maxSpeed), - maxSpeed, - initialPose); - } -} diff --git a/src/main/java/swervelib/parser/deserializer/PIDFRange.java b/src/main/java/swervelib/parser/deserializer/PIDFRange.java deleted file mode 100644 index 57b3d3ad..00000000 --- a/src/main/java/swervelib/parser/deserializer/PIDFRange.java +++ /dev/null @@ -1,10 +0,0 @@ -package swervelib.parser.deserializer; - -/** Class to hold the minimum and maximum input or output of the PIDF. */ -public class PIDFRange { - - /** Minimum value. */ - public double min = -1; - /** Maximum value. */ - public double max = 1; -} diff --git a/src/main/java/swervelib/parser/deserializer/ReflectionsManager.java b/src/main/java/swervelib/parser/deserializer/ReflectionsManager.java deleted file mode 100644 index ec10b8d5..00000000 --- a/src/main/java/swervelib/parser/deserializer/ReflectionsManager.java +++ /dev/null @@ -1,82 +0,0 @@ -package swervelib.parser.deserializer; - -/** Create classes only if the vendor dep exists. */ -public class ReflectionsManager { - - /** Vendors that supply their own vendordep to communicate with their products. */ - public enum VENDOR { - /** REVLib */ - REV, - /** CTRE Phoenix 5 and 6 */ - PHOENIX5, - PHOENIX6, - /** ThriftyLib */ - THRIFTYBOT, - /** StudicaLib */ - STUDICA - } - - /** - * Check if the vendordep exists. - * - * @param vendor Vendor to check for their library. - * @return Boolean on existence of their library. - */ - public static boolean checkIfVendorLibExists(VENDOR vendor) { - try { - Class library; - switch (vendor) { - case REV -> { - library = Class.forName("com.revrobotics.spark.SparkBase"); - break; - } - case PHOENIX6 -> { - library = Class.forName("com.ctre.phoenix6.hardware.TalonFXS"); - break; - } - case PHOENIX5 -> { - library = Class.forName("com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX"); - break; - } - case THRIFTYBOT -> { - library = Class.forName("com.thethriftybot.ThriftyNova"); - break; - } - case STUDICA -> { - library = Class.forName("com.studica.frc.AHRS"); - break; - } - default -> { - return false; - } - } - } catch (Exception e) { - return false; - } - return true; - } - - /** - * Create objects if the vendordep exists. Throw an exception when they dont. - * - * @param v Vendor to check if the vendordep exists. - * @param className Wrapper classname to create. - * @param parameterTypes Parameter types for the wrappers constructor. - * @param parameters Parameters for the wrappers constructor - * @param Wrapper type. - * @return Wrapper object. - */ - public static T create( - VENDOR v, String className, Class[] parameterTypes, Object[] parameters) { - if (!checkIfVendorLibExists(v)) { - throw new RuntimeException("Vendor " + v + " library not found! Please install it!"); - } - try { - Class wrapper = Class.forName(className); - Object vendorObj = wrapper.getDeclaredConstructor(parameterTypes).newInstance(parameters); - return (T) vendorObj; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/java/swervelib/parser/deserializer/package-info.java b/src/main/java/swervelib/parser/deserializer/package-info.java deleted file mode 100644 index 8a176651..00000000 --- a/src/main/java/swervelib/parser/deserializer/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Deserialize specific variables for outside the parser. */ -package swervelib.parser.deserializer; diff --git a/src/main/java/swervelib/parser/json/ControllerPropertiesJson.java b/src/main/java/swervelib/parser/json/ControllerPropertiesJson.java deleted file mode 100644 index 1b767faa..00000000 --- a/src/main/java/swervelib/parser/json/ControllerPropertiesJson.java +++ /dev/null @@ -1,30 +0,0 @@ -package swervelib.parser.json; - -import swervelib.parser.PIDFConfig; -import swervelib.parser.SwerveControllerConfiguration; -import swervelib.parser.SwerveDriveConfiguration; - -/** {@link swervelib.SwerveController} parsed class. Used to access the JSON data. */ -public class ControllerPropertiesJson { - - /** - * The minimum radius of the angle control joystick to allow for heading adjustment of the robot. - */ - public double angleJoystickRadiusDeadband; - /** The PID used to control the robot heading. */ - public PIDFConfig heading; - - /** - * Create the {@link SwerveControllerConfiguration} based on parsed and given data. - * - * @param driveConfiguration {@link SwerveDriveConfiguration} parsed configuration. - * @param maxSpeedMPS Maximum speed in meters per second for the angular acceleration of the - * robot. - * @return {@link SwerveControllerConfiguration} object based on parsed data. - */ - public SwerveControllerConfiguration createControllerConfiguration( - SwerveDriveConfiguration driveConfiguration, double maxSpeedMPS) { - return new SwerveControllerConfiguration( - driveConfiguration, heading, angleJoystickRadiusDeadband, maxSpeedMPS); - } -} diff --git a/src/main/java/swervelib/parser/json/DeviceJson.java b/src/main/java/swervelib/parser/json/DeviceJson.java deleted file mode 100644 index ab695d28..00000000 --- a/src/main/java/swervelib/parser/json/DeviceJson.java +++ /dev/null @@ -1,301 +0,0 @@ -package swervelib.parser.json; - -import static swervelib.telemetry.SwerveDriveTelemetry.canIdWarning; -import static swervelib.telemetry.SwerveDriveTelemetry.i2cLockupWarning; -import static swervelib.telemetry.SwerveDriveTelemetry.serialCommsIssueWarning; - -import com.ctre.phoenix.motorcontrol.FeedbackDevice; -import com.studica.frc.AHRS.NavXComType; -import edu.wpi.first.math.system.plant.DCMotor; -import edu.wpi.first.wpilibj.DriverStation; -import swervelib.encoders.AnalogAbsoluteEncoderSwerve; -import swervelib.encoders.CANCoderSwerve; -import swervelib.encoders.CanAndMagSwerve; -import swervelib.encoders.DIODutyCycleEncoderSwerve; -import swervelib.encoders.SparkFlexEncoderSwerve; -import swervelib.encoders.SparkMaxAnalogEncoderSwerve; -import swervelib.encoders.SparkMaxEncoderSwerve; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.encoders.TalonSRXEncoderSwerve; -import swervelib.imu.ADIS16448Swerve; -import swervelib.imu.ADIS16470Swerve; -import swervelib.imu.ADXRS450Swerve; -import swervelib.imu.AnalogGyroSwerve; -import swervelib.imu.CanandgyroSwerve; -import swervelib.imu.NavXSwerve; -import swervelib.imu.Pigeon2Swerve; -import swervelib.imu.PigeonSwerve; -import swervelib.imu.PigeonViaTalonSRXSwerve; -import swervelib.imu.SwerveIMU; -import swervelib.motors.SparkFlexSwerve; -import swervelib.motors.SparkMaxBrushedMotorSwerve; -import swervelib.motors.SparkMaxBrushedMotorSwerve.Type; -import swervelib.motors.SparkMaxSwerve; -import swervelib.motors.SwerveMotor; -import swervelib.motors.TalonFXSSwerve; -import swervelib.motors.TalonFXSwerve; -import swervelib.motors.TalonSRXSwerve; -import swervelib.parser.deserializer.ReflectionsManager; -import swervelib.parser.deserializer.ReflectionsManager.VENDOR; - -/** Device JSON parsed class. Used to access the JSON data. */ -public class DeviceJson { - - /** The device type, e.g. pigeon/pigeon2/sparkmax/talonfx/navx */ - public String type; - /** The CAN ID or pin ID of the device. */ - public int id; - /** The CAN bus name which the device resides on if using CAN. */ - public String canbus = ""; - - /** - * Create a {@link SwerveAbsoluteEncoder} from the current configuration. - * - * @param motor {@link SwerveMotor} of which attached encoders will be created from, only used - * when the type is "attached" or "canandencoder". - * @return {@link SwerveAbsoluteEncoder} given. - */ - public SwerveAbsoluteEncoder createEncoder(SwerveMotor motor) { - if (id > 40) { - canIdWarning.set(true); - } - switch (type) { - case "none": - return null; - case "integrated": - case "attached": - case "canandmag": - case "canandcoder": - return new SparkMaxEncoderSwerve(motor, 360); - case "sparkmax_analog": - return new SparkMaxAnalogEncoderSwerve(motor, 3.3); - case "sparkmax_analog5v": - return new SparkMaxAnalogEncoderSwerve(motor, 5); - case "sparkflex_integrated": - case "sparkflex_attached": - case "sparkflex_canandmag": - case "sparkflex_canandcoder": - return new SparkFlexEncoderSwerve(motor, 360); - case "canandcoder_can": - case "canandmag_can": - return new CanAndMagSwerve(id); - case "ctre_mag": - case "rev_hex": - case "throughbore": - case "am_mag": - case "dutycycle": - return new DIODutyCycleEncoderSwerve(id); - case "thrifty": - case "ma3": - case "analog": - return new AnalogAbsoluteEncoderSwerve(id); - case "cancoder": - return new CANCoderSwerve(id, canbus != null ? canbus : ""); - case "srxmag_standalone": - return new TalonSRXEncoderSwerve( - new TalonSRXSwerve(id, false, DCMotor.getCIM(1)), - FeedbackDevice.PulseWidthEncodedPosition); - case "talonsrx_pwm": - return new TalonSRXEncoderSwerve(motor, FeedbackDevice.PulseWidthEncodedPosition); - case "talonsrx_analog": - return new TalonSRXEncoderSwerve(motor, FeedbackDevice.Analog); - case "thrifty_nova_rev": - return ReflectionsManager.create( - VENDOR.THRIFTYBOT, - "swervelib.encoders.ThriftyNovaEncoderSwerve", - new Class[] {SwerveMotor.class, String.class}, - new Object[] {motor, "REV_ENCODER"}); - case "thrifty_nova_redux": - return ReflectionsManager.create( - VENDOR.THRIFTYBOT, - "swervelib.encoders.ThriftyNovaEncoderSwerve", - new Class[] {SwerveMotor.class, String.class}, - new Object[] {motor, "REDUX_ENCODER"}); - case "thrifty_nova_srx_mag": - return ReflectionsManager.create( - VENDOR.THRIFTYBOT, - "swervelib.encoders.ThriftyNovaEncoderSwerve", - new Class[] {SwerveMotor.class, String.class}, - new Object[] {motor, "SRX_MAG_ENCODER"}); - default: - throw new RuntimeException(type + " is not a recognized absolute encoder type."); - } - } - - /** - * Create a {@link SwerveIMU} from the given configuration. - * - * @return {@link SwerveIMU} given. - */ - public SwerveIMU createIMU() { - if (id > 40) { - canIdWarning.set(true); - } - switch (type) { - case "adis16448": - return new ADIS16448Swerve(); - case "adis16470": - return new ADIS16470Swerve(); - case "adxrs450": - return new ADXRS450Swerve(); - case "analog": - return new AnalogGyroSwerve(id); - case "canandgyro": - return new CanandgyroSwerve(id); - case "navx": - case "navx_spi": - return new NavXSwerve(NavXComType.kMXP_SPI); - case "navx_i2c": - DriverStation.reportWarning( - "WARNING: There exists an I2C lockup issue on the roboRIO that could occur, more information here: " - + "\nhttps://docs.wpilib.org/en/stable/docs/yearly-overview/known-issues" - + ".html#onboard-i2c-causing-system-lockups", - false); - i2cLockupWarning.set(true); - return new NavXSwerve(NavXComType.kI2C); - case "navx_usb": - DriverStation.reportWarning( - "WARNING: There is issues when using USB camera's and the NavX like this!\n" - + "https://pdocs.kauailabs.com/navx-mxp/guidance/selecting-an-interface/", - false); - serialCommsIssueWarning.set(true); - return new NavXSwerve(NavXComType.kUSB1); - case "navx_mxp_serial": - serialCommsIssueWarning.set(true); - return new NavXSwerve(NavXComType.kMXP_UART); - case "pigeon": - return new PigeonSwerve(id); - case "pigeon_via_talonsrx": - return new PigeonViaTalonSRXSwerve(id); - case "pigeon2": - return new Pigeon2Swerve(id, canbus != null ? canbus : ""); - default: - throw new RuntimeException(type + " is not a recognized imu/gyroscope type."); - } - } - - /** - * Create a {@link SwerveMotor} from the given configuration. - * - * @param isDriveMotor If the motor being generated is a drive motor. - * @return {@link SwerveMotor} given. - */ - public SwerveMotor createMotor(boolean isDriveMotor) { - if (id > 40) { - canIdWarning.set(true); - } - switch (type) { - case "talonfxs_neo": - return new TalonFXSSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getNEO(1)); - case "talonfxs_neo550": - return new TalonFXSSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getNeo550(1)); - case "talonfxs_vortex": - return new TalonFXSSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getNeoVortex(1)); - case "talonfxs_minion": - throw new UnsupportedOperationException( - "Cannot create minion combination yet"); // new TalonFXSSwerve(id, canbus != null ? - // canbus : "", isDriveMotor, - // DCMotor.getNeoVortex(1)); - case "sparkmax_neo": - case "neo": - case "sparkmax": - return new SparkMaxSwerve(id, isDriveMotor, DCMotor.getNEO(1)); - case "sparkmax_vortex": - return new SparkMaxSwerve(id, isDriveMotor, DCMotor.getNeoVortex(1)); - case "sparkmax_minion": - throw new UnsupportedOperationException("Cannot create minion combination yet"); - case "sparkmax_neo550": - case "neo550": - return new SparkMaxSwerve(id, isDriveMotor, DCMotor.getNeo550(1)); - case "sparkflex_vortex": - case "vortex": - case "sparkflex": - return new SparkFlexSwerve(id, isDriveMotor, DCMotor.getNeoVortex(1)); - case "sparkflex_neo": - return new SparkFlexSwerve(id, isDriveMotor, DCMotor.getNEO(1)); - case "sparkflex_neo550": - return new SparkFlexSwerve(id, isDriveMotor, DCMotor.getNeo550(1)); - case "sparkflex_minion": - throw new UnsupportedOperationException("Cannot create minion combination yet"); - case "falcon500": - case "falcon": - return new TalonFXSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getFalcon500(1)); - case "falcon500foc": - return new TalonFXSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getFalcon500Foc(1)); - case "krakenx60": - case "talonfx": - return new TalonFXSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getKrakenX60(1)); - case "krakenx60foc": - return new TalonFXSwerve( - id, canbus != null ? canbus : "", isDriveMotor, DCMotor.getKrakenX60Foc(1)); - case "talonsrx": - return new TalonSRXSwerve(id, isDriveMotor, DCMotor.getCIM(1)); - case "sparkmax_brushed": - if (canbus == null) { - canbus = ""; - } - switch (canbus) { - case "greyhill_63r256": - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kQuadrature, 1024, false, DCMotor.getCIM(1)); - case "srx_mag_encoder": - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kQuadrature, 4096, false, DCMotor.getCIM(1)); - case "throughbore": - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kQuadrature, 8192, false, DCMotor.getCIM(1)); - case "throughbore_dataport": - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kNoSensor, 8192, true, DCMotor.getCIM(1)); - case "greyhill_63r256_dataport": - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kQuadrature, 1024, true, DCMotor.getCIM(1)); - case "srx_mag_encoder_dataport": - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kQuadrature, 4096, true, DCMotor.getCIM(1)); - default: - if (isDriveMotor) { - throw new RuntimeException( - "Spark MAX " + id + " MUST have a encoder attached to the motor controller."); - } - // We are creating a motor for an angle motor which will use the absolute encoder - // attached to the data port. - return new SparkMaxBrushedMotorSwerve( - id, isDriveMotor, Type.kNoSensor, 0, false, DCMotor.getCIM(1)); - } - case "nova_neo": - return ReflectionsManager.create( - VENDOR.THRIFTYBOT, - "swervelib.motors.ThriftyNovaSwerve", - new Class[] {int.class, boolean.class, DCMotor.class}, - new Object[] {id, isDriveMotor, DCMotor.getNEO(1)}); - - case "nova_neo550": - return ReflectionsManager.create( - VENDOR.THRIFTYBOT, - "swervelib.motors.ThriftyNovaSwerve", - new Class[] {int.class, boolean.class, DCMotor.class}, - new Object[] {id, isDriveMotor, DCMotor.getNeo550(1)}); - - case "nova_vortex": - return ReflectionsManager.create( - VENDOR.THRIFTYBOT, - "swervelib.motors.ThriftyNovaSwerve", - new Class[] {int.class, boolean.class, DCMotor.class}, - new Object[] {id, isDriveMotor, DCMotor.getNeoVortex(1)}); - - case "nova_minion": - throw new UnsupportedOperationException( - "Cannot create minion combination"); // return new ThriftyNovaSwerve(id, isDriveMotor, - // DCMotor.getMinion(1)); - default: - throw new RuntimeException(type + " is not a recognized motor type."); - } - } -} diff --git a/src/main/java/swervelib/parser/json/ModuleJson.java b/src/main/java/swervelib/parser/json/ModuleJson.java deleted file mode 100644 index d0d70bf8..00000000 --- a/src/main/java/swervelib/parser/json/ModuleJson.java +++ /dev/null @@ -1,122 +0,0 @@ -package swervelib.parser.json; - -import com.revrobotics.spark.SparkMax; -import edu.wpi.first.math.util.Units; -import swervelib.encoders.SparkMaxEncoderSwerve; -import swervelib.encoders.SwerveAbsoluteEncoder; -import swervelib.encoders.ThriftyNovaEncoderSwerve; -import swervelib.motors.SwerveMotor; -import swervelib.motors.ThriftyNovaSwerve; -import swervelib.parser.PIDFConfig; -import swervelib.parser.SwerveModuleConfiguration; -import swervelib.parser.SwerveModulePhysicalCharacteristics; -import swervelib.parser.json.modules.BoolMotorJson; -import swervelib.parser.json.modules.ConversionFactorsJson; -import swervelib.parser.json.modules.LocationJson; - -/** {@link swervelib.SwerveModule} JSON parsed class. Used to access the JSON data. */ -public class ModuleJson { - - /** Drive motor device configuration. */ - public DeviceJson drive; - /** Angle motor device configuration. */ - public DeviceJson angle; - /** Conversion Factors composition. Auto-calculates the conversion factors. */ - public ConversionFactorsJson conversionFactors = new ConversionFactorsJson(); - /** Absolute encoder device configuration. */ - public DeviceJson encoder; - /** Defines which motors are inverted. */ - public BoolMotorJson inverted; - /** Absolute encoder offset from 0 in degrees. */ - public double absoluteEncoderOffset; - /** Absolute encoder inversion state. */ - public boolean absoluteEncoderInverted = false; - /** The location of the swerve module from the center of the robot in inches. */ - public LocationJson location; - /** Should do cosine compensation when not pointing correct direction;. */ - public boolean useCosineCompensator = true; - - /** - * Create the swerve module configuration based off of parsed data. - * - * @param anglePIDF The PIDF values for the angle motor. - * @param velocityPIDF The velocity PIDF values for the drive motor. - * @param physicalCharacteristics Physical characteristics of the swerve module. - * @param name Module json filename. - * @return {@link SwerveModuleConfiguration} based on the provided data and parsed data. - */ - public SwerveModuleConfiguration createModuleConfiguration( - PIDFConfig anglePIDF, - PIDFConfig velocityPIDF, - SwerveModulePhysicalCharacteristics physicalCharacteristics, - String name) { - SwerveMotor angleMotor = angle.createMotor(false); - SwerveAbsoluteEncoder absEncoder = encoder.createEncoder(angleMotor); - - // Throw an error if module locations are improperly set - if (location.front == 0 && location.left == 0) { - throw new RuntimeException( - "Improper Module Location Settings!\n" - + "Your module location is set to 0 for both 'front' and 'left' values.\n" - + "Set the distance from the center of the robot to the center of the wheel in your module JSON file!"); - } - - // Set the conversion factors to null if they are both 0. - if (!conversionFactors.works() && physicalCharacteristics.conversionFactor == null) { - throw new RuntimeException( - "No Conversion Factor configured! Please create SwerveDrive using \n" - + "SwerveParser.createSwerveDrive(driveFeedforward, maxSpeed, angleMotorConversionFactor, driveMotorConversion)\n" - + "OR\n" - + "SwerveParser.createSwerveDrive(maxSpeed, angleMotorConversionFactor, driveMotorConversion)\n" - + "OR\n" - + "Set the conversion factor in physicalproperties.json OR the module JSON file." - + "REMEMBER: You can calculate the conversion factors using SwerveMath.calculateMetersPerRotation AND SwerveMath.calculateDegreesPerSteeringRotation\n"); - } else if (physicalCharacteristics.conversionFactor.works() && !conversionFactors.works()) { - conversionFactors = physicalCharacteristics.conversionFactor; - } else if (physicalCharacteristics.conversionFactor.works()) - // If both are defined, override 0 with the physical characterstics input. - { - conversionFactors.angle = - conversionFactors.isAngleEmpty() - ? physicalCharacteristics.conversionFactor.angle - : conversionFactors.angle; - conversionFactors.drive = - conversionFactors.isDriveEmpty() - ? physicalCharacteristics.conversionFactor.drive - : conversionFactors.drive; - } - - if (conversionFactors.isDriveEmpty() || conversionFactors.isAngleEmpty()) { - throw new RuntimeException( - "Conversion factors cannot be 0, please configure conversion factors in physicalproperties.json or the module JSON files."); - } - - // Backwards compatibility, auto-optimization. - if (conversionFactors.angle.factor == 360 - && absEncoder != null - && (absEncoder instanceof SparkMaxEncoderSwerve - && angleMotor.getMotor() instanceof SparkMax)) { - angleMotor.setAbsoluteEncoder(absEncoder); - } else if ((absEncoder instanceof ThriftyNovaEncoderSwerve - && angleMotor instanceof ThriftyNovaSwerve)) { - angleMotor.setAbsoluteEncoder(absEncoder); - } - - return new SwerveModuleConfiguration( - drive.createMotor(true), - angleMotor, - conversionFactors, - absEncoder, - absoluteEncoderOffset, - Units.inchesToMeters(Math.round(location.front)), - Units.inchesToMeters(Math.round(location.left)), - anglePIDF, - velocityPIDF, - physicalCharacteristics, - absoluteEncoderInverted, - inverted.drive, - inverted.angle, - name.replaceAll("\\.json", ""), - useCosineCompensator); - } -} diff --git a/src/main/java/swervelib/parser/json/MotorConfigDouble.java b/src/main/java/swervelib/parser/json/MotorConfigDouble.java deleted file mode 100644 index fa0d1fc9..00000000 --- a/src/main/java/swervelib/parser/json/MotorConfigDouble.java +++ /dev/null @@ -1,24 +0,0 @@ -package swervelib.parser.json; - -/** Used to store doubles for motor configuration. */ -public class MotorConfigDouble { - - /** Drive motor. */ - public double drive; - /** Angle motor. */ - public double angle; - - /** Default constructor. */ - public MotorConfigDouble() {} - - /** - * Default constructor. - * - * @param angle Angle data. - * @param drive Drive data. - */ - public MotorConfigDouble(double angle, double drive) { - this.angle = angle; - this.drive = drive; - } -} diff --git a/src/main/java/swervelib/parser/json/MotorConfigInt.java b/src/main/java/swervelib/parser/json/MotorConfigInt.java deleted file mode 100644 index 6d191386..00000000 --- a/src/main/java/swervelib/parser/json/MotorConfigInt.java +++ /dev/null @@ -1,24 +0,0 @@ -package swervelib.parser.json; - -/** Used to store ints for motor configuration. */ -public class MotorConfigInt { - - /** Drive motor. */ - public int drive; - /** Angle motor. */ - public int angle; - - /** Default constructor. */ - public MotorConfigInt() {} - - /** - * Default constructor with values. - * - * @param drive Drive data. - * @param angle Angle data. - */ - public MotorConfigInt(int drive, int angle) { - this.angle = angle; - this.drive = drive; - } -} diff --git a/src/main/java/swervelib/parser/json/PIDFPropertiesJson.java b/src/main/java/swervelib/parser/json/PIDFPropertiesJson.java deleted file mode 100644 index 929dea40..00000000 --- a/src/main/java/swervelib/parser/json/PIDFPropertiesJson.java +++ /dev/null @@ -1,12 +0,0 @@ -package swervelib.parser.json; - -import swervelib.parser.PIDFConfig; - -/** {@link swervelib.SwerveModule} PID with Feedforward for the drive motor and angle motor. */ -public class PIDFPropertiesJson { - - /** The PIDF with Integral Zone used for the drive motor. */ - public PIDFConfig drive; - /** The PIDF with Integral Zone used for the angle motor. */ - public PIDFConfig angle; -} diff --git a/src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java b/src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java deleted file mode 100644 index 0b783f19..00000000 --- a/src/main/java/swervelib/parser/json/PhysicalPropertiesJson.java +++ /dev/null @@ -1,75 +0,0 @@ -package swervelib.parser.json; - -import static edu.wpi.first.units.Units.Kilogram; -import static edu.wpi.first.units.Units.Pounds; - -import swervelib.parser.SwerveModulePhysicalCharacteristics; -import swervelib.parser.json.modules.ConversionFactorsJson; - -/** - * {@link swervelib.parser.SwerveModulePhysicalCharacteristics} parsed data. Used to configure the - * SwerveModule. - */ -public class PhysicalPropertiesJson { - - /** DEPRECATED! Use {@link PhysicalPropertiesJson#conversionFactors} instead. */ - @Deprecated(since = "2025", forRemoval = true) - public MotorConfigDouble conversionFactor = new MotorConfigDouble(); - /** Minimum voltage to spin the module or wheel. */ - public MotorConfigDouble friction = new MotorConfigDouble(0.3, 0.2); - /** Steer rotational inertia in KilogramMetersSquare. */ - public double steerRotationalInertia = 0.03; - /** Robot mass in lb (pounds) */ - public double robotMass = 110.2311; - /** Conversion Factors composition. Auto-calculates the conversion factors. */ - public ConversionFactorsJson conversionFactors = new ConversionFactorsJson(); - /** The current limit in AMPs to apply to the motors. */ - public MotorConfigInt currentLimit = new MotorConfigInt(40, 20); - /** The minimum number of seconds to take for the motor to go from 0 to full throttle. */ - public MotorConfigDouble rampRate = new MotorConfigDouble(0.25, 0.25); - /** - * The grip tape coefficient of friction on carpet. Used to calculate the practical maximum - * acceleration. - */ - public double wheelGripCoefficientOfFriction = 1.19; - /** The voltage to use for the smart motor voltage compensation, default is 12. */ - public double optimalVoltage = 12; - - /** - * Create the physical characteristics based off the parsed data. - * - * @return {@link SwerveModulePhysicalCharacteristics} based on parsed data. - */ - public SwerveModulePhysicalCharacteristics createPhysicalProperties() { - // Setup deprecation notice. - if (conversionFactor.drive != 0 - && conversionFactor.angle != 0 - && conversionFactors.isDriveEmpty() - && conversionFactors.isAngleEmpty()) { - throw new RuntimeException( - "\n'conversionFactor': {'drive': " - + conversionFactor.drive - + ", 'angle': " - + conversionFactor.angle - + "} \nis deprecated, please use\n" - + "'conversionFactors': {'drive': {'factor': " - + conversionFactor.drive - + "}, 'angle': {'factor': " - + conversionFactor.angle - + "} }"); - } - - return new SwerveModulePhysicalCharacteristics( - conversionFactors, - wheelGripCoefficientOfFriction, - optimalVoltage, - currentLimit.drive, - currentLimit.angle, - rampRate.drive, - rampRate.angle, - friction.drive, - friction.angle, - steerRotationalInertia, - Pounds.of(robotMass).in(Kilogram)); - } -} diff --git a/src/main/java/swervelib/parser/json/SwerveDriveJson.java b/src/main/java/swervelib/parser/json/SwerveDriveJson.java deleted file mode 100644 index 0df3e695..00000000 --- a/src/main/java/swervelib/parser/json/SwerveDriveJson.java +++ /dev/null @@ -1,15 +0,0 @@ -package swervelib.parser.json; - -/** - * {@link swervelib.SwerveDrive} JSON parsed class. Used to access parsed data from the - * swervedrive.json file. - */ -public class SwerveDriveJson { - - /** Robot IMU used to determine heading of the robot. */ - public DeviceJson imu; - /** Invert the IMU of the robot. */ - public boolean invertedIMU; - /** Module JSONs in order clockwise order starting from front left. */ - public String[] modules; -} diff --git a/src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java b/src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java deleted file mode 100644 index f946f664..00000000 --- a/src/main/java/swervelib/parser/json/modules/AngleConversionFactorsJson.java +++ /dev/null @@ -1,27 +0,0 @@ -package swervelib.parser.json.modules; - -import swervelib.math.SwerveMath; - -/** Angle motor conversion factors composite JSON parse class. */ -public class AngleConversionFactorsJson { - - /** - * Gear ratio for the angle/steering/azimuth motor on the Swerve Module. Motor rotations to 1 - * wheel rotation. - */ - public double gearRatio; - /** Calculated or given conversion factor. */ - public double factor = 0; - - /** - * Calculate the drive conversion factor. - * - * @return Drive conversion factor, if factor isn't set. - */ - public double calculate() { - if (factor == 0) { - factor = SwerveMath.calculateDegreesPerSteeringRotation(gearRatio); - } - return factor; - } -} diff --git a/src/main/java/swervelib/parser/json/modules/BoolMotorJson.java b/src/main/java/swervelib/parser/json/modules/BoolMotorJson.java deleted file mode 100644 index 16dd1d8b..00000000 --- a/src/main/java/swervelib/parser/json/modules/BoolMotorJson.java +++ /dev/null @@ -1,10 +0,0 @@ -package swervelib.parser.json.modules; - -/** Inverted motor JSON parsed class. Used to access the JSON data. */ -public class BoolMotorJson { - - /** Drive motor inversion state. */ - public boolean drive; - /** Angle motor inversion state. */ - public boolean angle; -} diff --git a/src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java b/src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java deleted file mode 100644 index 8d69d3f9..00000000 --- a/src/main/java/swervelib/parser/json/modules/ConversionFactorsJson.java +++ /dev/null @@ -1,40 +0,0 @@ -package swervelib.parser.json.modules; - -/** Conversion Factors parsed JSON class */ -public class ConversionFactorsJson { - - /** Drive motor conversion factors composition. */ - public DriveConversionFactorsJson drive = new DriveConversionFactorsJson(); - /** Angle motor conversion factors composition. */ - public AngleConversionFactorsJson angle = new AngleConversionFactorsJson(); - - /** - * Check if the conversion factors are set for the drive motor. - * - * @return Empty - */ - public boolean isDriveEmpty() { - drive.calculate(); - return drive.factor == 0; - } - - /** - * Check if the conversion factors are set for the angle motor. - * - * @return Empty - */ - public boolean isAngleEmpty() { - angle.calculate(); - return angle.factor == 0; - } - - /** - * Check if the conversion factor can be found. - * - * @return If the conversion factors can be found. - */ - public boolean works() { - return (angle.factor != 0 && drive.factor != 0) - || ((drive.gearRatio != 0 && drive.diameter != 0)) && (angle.gearRatio != 0); - } -} diff --git a/src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java b/src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java deleted file mode 100644 index 55144f77..00000000 --- a/src/main/java/swervelib/parser/json/modules/DriveConversionFactorsJson.java +++ /dev/null @@ -1,29 +0,0 @@ -package swervelib.parser.json.modules; - -import edu.wpi.first.math.util.Units; -import swervelib.math.SwerveMath; - -/** Drive motor composite JSON parse class. */ -public class DriveConversionFactorsJson { - - /** Gear ratio for the drive motor rotations to turn the wheel 1 complete rotation. */ - public double gearRatio; - /** Diameter of the wheel in inches. */ - public double diameter; - /** Calculated conversion factor. */ - public double factor = 0; - - /** - * Calculate the drive conversion factor. - * - * @return Drive conversion factor, if factor isn't set. - */ - public double calculate() { - if (factor == 0) { - factor = - SwerveMath.calculateMetersPerRotation( - Units.inchesToMeters(this.diameter), this.gearRatio); - } - return factor; - } -} diff --git a/src/main/java/swervelib/parser/json/modules/LocationJson.java b/src/main/java/swervelib/parser/json/modules/LocationJson.java deleted file mode 100644 index d7953258..00000000 --- a/src/main/java/swervelib/parser/json/modules/LocationJson.java +++ /dev/null @@ -1,14 +0,0 @@ -package swervelib.parser.json.modules; - -/** - * Location JSON parsed class. Used to access the JSON data. Module locations, in inches, as - * distances to the center of the robot. +x is towards the robot front, and +y is towards robot - * left. - */ -public class LocationJson { - - /** Location of the swerve module in inches from the center of the robot horizontally. */ - public double front = 0; - /** Location of the swerve module in inches from the center of the robot vertically. */ - public double left = 0; -} diff --git a/src/main/java/swervelib/parser/json/modules/package-info.java b/src/main/java/swervelib/parser/json/modules/package-info.java deleted file mode 100644 index f6e0ab73..00000000 --- a/src/main/java/swervelib/parser/json/modules/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** JSON Mapped Configuration types for modules. */ -package swervelib.parser.json.modules; diff --git a/src/main/java/swervelib/parser/json/package-info.java b/src/main/java/swervelib/parser/json/package-info.java deleted file mode 100644 index 776e4150..00000000 --- a/src/main/java/swervelib/parser/json/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** JSON Mapped classes for parsing configuration files. */ -package swervelib.parser.json; diff --git a/src/main/java/swervelib/parser/package-info.java b/src/main/java/swervelib/parser/package-info.java deleted file mode 100644 index 60372be5..00000000 --- a/src/main/java/swervelib/parser/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** JSON Parser for YAGSL configurations. */ -package swervelib.parser; diff --git a/src/main/java/swervelib/simulation/SwerveIMUSimulation.java b/src/main/java/swervelib/simulation/SwerveIMUSimulation.java deleted file mode 100644 index 2bc2ef17..00000000 --- a/src/main/java/swervelib/simulation/SwerveIMUSimulation.java +++ /dev/null @@ -1,98 +0,0 @@ -package swervelib.simulation; - -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.math.kinematics.SwerveDriveKinematics; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.wpilibj.smartdashboard.Field2d; -import java.util.Optional; -import org.ironmaple.simulation.drivesims.GyroSimulation; - -/** Simulation for {@link swervelib.SwerveDrive} IMU. */ -public class SwerveIMUSimulation { - - private final GyroSimulation gyroSimulation; - - /** - * Create the swerve drive IMU simulation. - * - * @param gyroSimulation Gyro simulation from MapleSim. - */ - public SwerveIMUSimulation(GyroSimulation gyroSimulation) { - this.gyroSimulation = gyroSimulation; - } - - /** - * Get the estimated angle of the robot. - * - * @return {@link Rotation2d} estimation of the robot. - */ - public Rotation2d getYaw() { - return gyroSimulation.getGyroReading(); - } - - /** - * Pitch is not simulated currently, always returns 0. - * - * @return Pitch of the robot as {@link Rotation2d}. - */ - public Rotation2d getPitch() { - return new Rotation2d(); - } - - /** - * Roll is not simulated currently, always returns 0. - * - * @return Roll of the robot as {@link Rotation2d}. - */ - public Rotation2d getRoll() { - return new Rotation2d(); - } - - /** - * Gets the estimated gyro {@link Rotation3d} of the robot. - * - * @return The heading as a {@link Rotation3d} angle - */ - public Rotation3d getGyroRotation3d() { - return new Rotation3d(0, 0, getYaw().getRadians()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in m/s/s. If acceleration isn't supported returns - * empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - public Optional getAccel() { - return Optional.empty(); - } - - /** - * Update the odometry of the simulated {@link swervelib.SwerveDrive} and post the {@link - * swervelib.SwerveModule} states to the {@link Field2d}. - * - * @param kinematics {@link SwerveDriveKinematics} of the swerve drive. - * @param states {@link SwerveModuleState} array of the module states. - * @param modulePoses {@link Pose2d} representing the swerve modules. - * @param field {@link Field2d} to update. - */ - public void updateOdometry( - SwerveDriveKinematics kinematics, - SwerveModuleState[] states, - Pose2d[] modulePoses, - Field2d field) { - field.getObject("XModules").setPoses(modulePoses); - } - - /** - * Set the heading of the robot. - * - * @param angle Angle of the robot in radians. - */ - public void setAngle(double angle) { - this.gyroSimulation.setRotation(Rotation2d.fromRadians(angle)); - } -} diff --git a/src/main/java/swervelib/simulation/SwerveModuleSimulation.java b/src/main/java/swervelib/simulation/SwerveModuleSimulation.java deleted file mode 100644 index 4b147d32..00000000 --- a/src/main/java/swervelib/simulation/SwerveModuleSimulation.java +++ /dev/null @@ -1,91 +0,0 @@ -package swervelib.simulation; - -import static edu.wpi.first.units.Units.Amps; - -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import org.ironmaple.simulation.drivesims.SelfControlledSwerveDriveSimulation; -import swervelib.SwerveDrive; -import swervelib.parser.SwerveModulePhysicalCharacteristics; - -/** Class that wraps around {@link org.ironmaple.simulation.drivesims.SwerveModuleSimulation} */ -public class SwerveModuleSimulation { - - /** MapleSim module. */ - public SelfControlledSwerveDriveSimulation.SelfControlledModuleSimulation mapleSimModule = null; - - /** - * Configure the maple sim module - * - * @param simModule the {@link org.ironmaple.simulation.drivesims.SwerveModuleSimulation} object - * for simulation - * @param physicalCharacteristics Physical characteristics of the swerve drive from the JSON or - * built. - */ - public void configureSimModule( - org.ironmaple.simulation.drivesims.SwerveModuleSimulation simModule, - SwerveModulePhysicalCharacteristics physicalCharacteristics) { - this.mapleSimModule = - new SelfControlledSwerveDriveSimulation.SelfControlledModuleSimulation(simModule); - this.mapleSimModule.withCurrentLimits( - Amps.of(physicalCharacteristics.driveMotorCurrentLimit), - Amps.of(physicalCharacteristics.angleMotorCurrentLimit)); - } - - /** - * Update the position and state of the module. Called from {@link - * swervelib.SwerveModule#setDesiredState} function when simulated. - * - * @param desiredState State the swerve module is set to. - */ - public void updateStateAndPosition(SwerveModuleState desiredState) { - mapleSimModule.runModuleState(desiredState); - } - - /** - * Runs a drive motor characterization on the sim module. This is called from {@link - * swervelib.SwerveDriveTest#runDriveMotorsCharacterizationOnSimModules(SwerveDrive, double, - * boolean)} to run sysId during simulation - * - * @param desiredFacing the desired facing of the module - * @param volts the voltage to run - */ - public void runDriveMotorCharacterization(Rotation2d desiredFacing, double volts) { - mapleSimModule.runDriveMotorCharacterization(desiredFacing, volts); - } - - /** - * Runs a drive motor characterization on the sim module. This method is called from {@link - * swervelib.SwerveDriveTest#runAngleMotorsCharacterizationOnSimModules(SwerveDrive, double)} to - * run sysId during simulation - * - * @param volts the voltage to run - */ - public void runAngleMotorCharacterization(double volts) { - mapleSimModule.runSteerMotorCharacterization(volts); - } - - /** - * Get the simulated swerve module position. - * - * @return {@link SwerveModulePosition} of the simulated module. - */ - public SwerveModulePosition getPosition() { - return mapleSimModule.getModulePosition(); - } - - /** - * Get the {@link SwerveModuleState} of the simulated module. - * - * @return {@link SwerveModuleState} of the simulated module. - */ - public SwerveModuleState getState() { - if (mapleSimModule == null) { - return new SwerveModuleState(); - } - SwerveModuleState state = mapleSimModule.getMeasuredState(); - state.angle = state.angle.minus(Rotation2d.kZero); - return state; - } -} diff --git a/src/main/java/swervelib/simulation/package-info.java b/src/main/java/swervelib/simulation/package-info.java deleted file mode 100644 index 908fc590..00000000 --- a/src/main/java/swervelib/simulation/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Classes used to simulate the swerve drive. */ -package swervelib.simulation; diff --git a/src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java b/src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java deleted file mode 100644 index a0b03e36..00000000 --- a/src/main/java/swervelib/telemetry/SwerveDriveTelemetry.java +++ /dev/null @@ -1,346 +0,0 @@ -package swervelib.telemetry; - -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.networktables.DoubleArrayPublisher; -import edu.wpi.first.networktables.DoublePublisher; -import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.networktables.StringPublisher; -import edu.wpi.first.networktables.StructArrayPublisher; -import edu.wpi.first.networktables.StructPublisher; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import edu.wpi.first.wpilibj.DriverStation; -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj.Timer; -import swervelib.SwerveDrive; - -/** - * Telemetry to describe the {@link swervelib.SwerveDrive} following frc-web-components. (Which - * follows AdvantageKit) - */ -public class SwerveDriveTelemetry { - - /** An {@link Alert} for if the CAN ID is greater than 40. */ - public static final Alert canIdWarning = - new Alert( - "JSON", - "CAN IDs greater than 40 can cause undefined behaviour, please use a CAN ID below 40!", - AlertType.kWarning); - /** An {@link Alert} for if there is an I2C lockup issue on the roboRIO. */ - public static final Alert i2cLockupWarning = - new Alert( - "IMU", - "I2C lockup issue detected on roboRIO. Check console for more information.", - AlertType.kWarning); - /** NavX serial comm issue. */ - public static final Alert serialCommsIssueWarning = - new Alert( - "IMU", - "Serial comms is interrupted with USB and other serial traffic and causes intermittent connected/disconnection issues. Please consider another protocol or be mindful of this.", - AlertType.kWarning); - /** Module counter publisher for NT4 */ - private static final DoublePublisher moduleCountPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/moduleCount") - .publish(); - /** Module measured states for Nt4 */ - private static final DoubleArrayPublisher measuredStatesArrayPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleArrayTopic("swerve/measuredStates") - .publish(); - /** Desired states for NT4 */ - private static final DoubleArrayPublisher desiredStatesArrayPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleArrayTopic("swerve/desiredStates") - .publish(); - /** Measured chassis speeds array publisher. */ - private static final DoubleArrayPublisher measuredChassisSpeedsArrayPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleArrayTopic("swerve/measuredChassisSpeeds") - .publish(); - /** Desired chassis speeds array publisher. */ - private static final DoubleArrayPublisher desiredChassisSpeedsArrayPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleArrayTopic("swerve/desiredChassisSpeeds") - .publish(); - /** Robot rotation publisher. */ - private static final DoublePublisher robotRotationPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/robotRotation") - .publish(); - /** Max angular velocity publisher. */ - private static final DoublePublisher maxAngularVelocityPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/maxAngularVelocity") - .publish(); - /** Struct publisher for AdvantageScope swerve widgets. */ - private static final StructArrayPublisher measuredStatesStruct = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStructArrayTopic("swerve/advantagescope/currentStates", SwerveModuleState.struct) - .publish(); - /** Struct publisher for AdvantageScope swerve widgets. */ - private static final StructArrayPublisher desiredStatesStruct = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStructArrayTopic("swerve/advantagescope/desiredStates", SwerveModuleState.struct) - .publish(); - /** Measured {@link ChassisSpeeds} for NT4 AdvantageScope swerve widgets. */ - private static final StructPublisher measuredChassisSpeedsStruct = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStructTopic("swerve/advantagescope/measuredChassisSpeeds", ChassisSpeeds.struct) - .publish(); - /** Desired {@link ChassisSpeeds} for NT4 AdvantageScope swerve widgets. */ - private static final StructPublisher desiredChassisSpeedsStruct = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStructTopic("swerve/advantagescope/desiredChassisSpeeds", ChassisSpeeds.struct) - .publish(); - /** Robot {@link Rotation2d} for AdvantageScope swerve widgets. */ - private static final StructPublisher robotRotationStruct = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStructTopic("swerve/advantagescope/robotRotation", Rotation2d.struct) - .publish(); - /** Wheel locations array publisher for NT4. */ - private static final DoubleArrayPublisher wheelLocationsArrayPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleArrayTopic("swerve/wheelLocation") - .publish(); - /** Max speed publisher for NT4. */ - private static final DoublePublisher maxSpeedPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/maxSpeed") - .publish(); - /** Rotation unit for NT4. */ - private static final StringPublisher rotationUnitPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStringTopic("swerve/rotationUnit") - .publish(); - /** Chassis width publisher */ - private static final DoublePublisher sizeLeftRightPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/sizeLeftRight") - .publish(); - /** Chassis Length publisher. */ - private static final DoublePublisher sizeFrontBackPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/sizeFrontBack") - .publish(); - /** Chassis direction widget publisher. */ - private static final StringPublisher forwardDirectionPublisher = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getStringTopic("swerve/forwardDirection") - .publish(); - /** Odometry cycle time, updated whenever {@link SwerveDrive#updateOdometry()} is called. */ - private static final DoublePublisher odomCycleTime = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/odomCycleMS") - .publish(); - /** - * Control cycle time, updated whenever {@link - * swervelib.SwerveModule#setDesiredState(SwerveModuleState, boolean, double)} is called for the - * last module. - */ - private static final DoublePublisher ctrlCycleTime = - NetworkTableInstance.getDefault() - .getTable("SmartDashboard") - .getDoubleTopic("swerve/controlCycleMS") - .publish(); - /** Odometry timer to track cycle times. */ - private static final Timer odomTimer = new Timer(); - /** Control timer to track cycle times. */ - private static final Timer ctrlTimer = new Timer(); - /** Measured swerve module states object. */ - public static SwerveModuleState[] measuredStatesObj = new SwerveModuleState[4]; - /** Desired swerve module states object */ - public static SwerveModuleState[] desiredStatesObj = new SwerveModuleState[4]; - /** - * The maximum achievable angular velocity of the robot. This is used to visualize the angular - * velocity from the chassis speeds properties. - */ - public static ChassisSpeeds measuredChassisSpeedsObj = new ChassisSpeeds(); - /** Describes the desired forward, sideways and angular velocity of the robot. */ - public static ChassisSpeeds desiredChassisSpeedsObj = new ChassisSpeeds(); - /** The robot's current rotation based on odometry or gyro readings */ - public static Rotation2d robotRotationObj = new Rotation2d(); - /** The current telemetry verbosity level. */ - public static TelemetryVerbosity verbosity = TelemetryVerbosity.MACHINE; - /** State of simulation of the Robot, used to optimize retrieval. */ - public static boolean isSimulation = RobotBase.isSimulation(); - /** The number of swerve modules */ - public static int moduleCount; - /** The Locations of the swerve drive wheels. */ - public static double[] wheelLocations; - /** - * An array of rotation and velocity values describing the measured state of each swerve module - */ - public static double[] measuredStates; - /** An array of rotation and velocity values describing the desired state of each swerve module */ - public static double[] desiredStates; - /** The robot's current rotation based on odometry or gyro readings */ - public static double robotRotation = 0; - /** The maximum achievable speed of the modules, used to adjust the size of the vectors. */ - public static double maxSpeed; - /** The units of the module rotations and robot rotation */ - public static String rotationUnit = "degrees"; - /** The distance between the left and right modules. */ - public static double sizeLeftRight; - /** The distance between the front and back modules. */ - public static double sizeFrontBack; - /** - * The direction the robot should be facing when the "Robot Rotation" is zero or blank. This - * option is often useful to align with odometry data or match videos. 'up', 'right', 'down' or - * 'left' - */ - public static String forwardDirection = "up"; - /** - * The maximum achievable angular velocity of the robot. This is used to visualize the angular - * velocity from the chassis speeds properties. - */ - public static double maxAngularVelocity; - /** - * The maximum achievable angular velocity of the robot. This is used to visualize the angular - * velocity from the chassis speeds properties. - */ - public static double[] measuredChassisSpeeds = new double[3]; - /** Describes the desired forward, sideways and angular velocity of the robot. */ - public static double[] desiredChassisSpeeds = new double[3]; - /** Update the telemetry settings that infrequently change. */ - public static boolean updateSettings = true; - - /** Start the ctrl timer to measure cycle time, independent of periodic loops. */ - public static void startCtrlCycle() { - if (ctrlTimer.isRunning()) { - ctrlTimer.reset(); - } else { - ctrlTimer.start(); - } - } - - /** Update the Control cycle time. */ - public static void endCtrlCycle() { - if (DriverStation.isTeleopEnabled() - || DriverStation.isAutonomousEnabled() - || DriverStation.isTestEnabled()) { - // 100ms per module on initialization is normal - ctrlCycleTime.set(ctrlTimer.get() * 1000); - } - ctrlTimer.reset(); - } - - /** - * Start the odom cycle timer to calculate how long each odom took. Independent of periodic loops. - */ - public static void startOdomCycle() { - if (odomTimer.isRunning()) { - - odomTimer.reset(); - } else { - odomTimer.start(); - } - } - - /** Update the odom cycle time. */ - public static void endOdomCycle() { - if (DriverStation.isTeleopEnabled() - || DriverStation.isAutonomousEnabled() - || DriverStation.isTestEnabled()) { - odomCycleTime.set(odomTimer.get() * 1000); - } - odomTimer.reset(); - } - - /** Update only the settings that infrequently or never change. */ - public static void updateSwerveTelemetrySettings() { - if (updateSettings) { - updateSettings = false; - wheelLocationsArrayPublisher.set(wheelLocations); - maxSpeedPublisher.set(maxSpeed); - rotationUnitPublisher.set(rotationUnit); - sizeLeftRightPublisher.set(sizeLeftRight); - sizeFrontBackPublisher.set(sizeFrontBack); - forwardDirectionPublisher.set(forwardDirection); - } - } - - /** Upload data to smartdashboard */ - public static void updateData() { - if (updateSettings) { - updateSwerveTelemetrySettings(); - } - measuredChassisSpeeds[0] = measuredChassisSpeedsObj.vxMetersPerSecond; - measuredChassisSpeeds[1] = measuredChassisSpeedsObj.vyMetersPerSecond; - measuredChassisSpeeds[2] = Math.toDegrees(measuredChassisSpeedsObj.omegaRadiansPerSecond); - - desiredChassisSpeeds[0] = desiredChassisSpeedsObj.vxMetersPerSecond; - desiredChassisSpeeds[1] = desiredChassisSpeedsObj.vyMetersPerSecond; - desiredChassisSpeeds[2] = Math.toDegrees(desiredChassisSpeedsObj.omegaRadiansPerSecond); - - robotRotation = robotRotationObj.getDegrees(); - - for (int i = 0; i < measuredStatesObj.length; i++) { - SwerveModuleState state = measuredStatesObj[i]; - if (state != null) { - measuredStates[i * 2] = state.angle.getDegrees(); - measuredStates[i * 2 + 1] = state.speedMetersPerSecond; - } - } - - for (int i = 0; i < desiredStatesObj.length; i++) { - SwerveModuleState state = desiredStatesObj[i]; - if (state != null) { - desiredStates[i * 2] = state.angle.getDegrees(); - desiredStates[i * 2 + 1] = state.speedMetersPerSecond; - } - } - - moduleCountPublisher.set(moduleCount); - measuredStatesArrayPublisher.set(measuredStates); - desiredStatesArrayPublisher.set(desiredStates); - robotRotationPublisher.set(robotRotation); - maxAngularVelocityPublisher.set(maxAngularVelocity); - - measuredChassisSpeedsArrayPublisher.set(measuredChassisSpeeds); - desiredChassisSpeedsArrayPublisher.set(desiredChassisSpeeds); - - desiredStatesStruct.set(desiredStatesObj); - measuredStatesStruct.set(measuredStatesObj); - desiredChassisSpeedsStruct.set(desiredChassisSpeedsObj); - measuredChassisSpeedsStruct.set(measuredChassisSpeedsObj); - robotRotationStruct.set(robotRotationObj); - } - - /** Verbosity of telemetry data sent back. */ - public enum TelemetryVerbosity { - /** No telemetry data is sent back. */ - NONE, - /** Low telemetry data, only post the robot position on the field. */ - LOW, - /** Medium telemetry data, swerve directory */ - INFO, - /** Info level + field info */ - POSE, - /** Full swerve drive data is sent back in both human and machine readable forms. */ - HIGH, - /** Only send the machine readable data related to swerve drive. */ - MACHINE - } -} diff --git a/src/main/java/swervelib/telemetry/package-info.java b/src/main/java/swervelib/telemetry/package-info.java deleted file mode 100644 index 2f47093d..00000000 --- a/src/main/java/swervelib/telemetry/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Telemetry package for sending data to NT4 or SmartDashboard. */ -package swervelib.telemetry; diff --git a/src/main/resources/calculateRobotToCameraTransform.py b/src/main/resources/calculateRobotToCameraTransform.py new file mode 100644 index 00000000..e88de02b --- /dev/null +++ b/src/main/resources/calculateRobotToCameraTransform.py @@ -0,0 +1,80 @@ +from wpimath.geometry import Pose3d, Transform3d, Translation3d, Rotation3d, Quaternion + +# Use this script for empirical calculation of robot to camera transform +# for each pose sensor configuration using fixed target measured +# from robot and targetPose averages from PhotonVision. + +# Steps: +# 1) Place the robot in a fixed position with the target camera facing +# a single AprilTag target with the center of robot precisely aligned +# with the center of the AprilTag (so that the Y translation below is zero). +# Best approach is to create a rectangle on the floor with gaffer tape +# with the robot frame parallel on one side and the AprilTag (mounted) +# on the other side to ensure the two are parallel. + +# 2) Accurately measure the distance from the center of the robot to +# the face of the AprilTag to set the X translation value below. + +# 3) Accurately measure the distance from the floor (base of the roobt) +# to the center of the AprilTag to set the Z translation value below. + +# 4) Using PhotonVision web UI, confirm that the target AprilTag is being +# detected and the target translation values are being reported correctly. + +# 5) Using AdvantageScope, connect to the robot and expand the photonvision +# topic, then expand the camera name being calculated, then expand to the +# "targetPose", and finally expand both translation and rotation/quaternion. +# Create a new "Statistics" panel in AdvantageScope with an empty "Measurements" +# section. Under both translation and rotation sections under targetPose, drag each +# individual value into the Measurements section so that the calculation of +# median averages can begin. Wait at least 30 seconds for the Median value for each +# translation (x, y, z) and rotation (w, x, y, z) property to settle. Use each of +# those Median values to set the appropriate constant value in the script +# below for target to camera transform calculation. + +# 6) After all of the script constant values have been set, save the script +# and open a new Terminal window in VSCode (do not commit the script changes in git). +# After the Python virtual environment activates, copy, paste, and execute the +# script with the following command: +# python .\lib\scripts\calculateRobotToCameraTransform.py + +# 7) Copy the Transform3d value that is printed to the terminal console and +# paste it into the appropriate place in the robot constants for the associated +# pose sensor configuration being calculated here. + +# Example output from example values below: +# Transform3d(Translation3d(0.149506, -0.055318, 0.271137), Rotation3d(-0.001852, -0.181301, 0.020370)) + +# 8) Repeat this same process from step 1 above for all other pose sensor cameras on the robot. + +# Note: the constant values below are for example only - see the steps above for setting real/correct values. + +TARGET_TO_ROBOT_TRANSLATION_X = 0.860 # distance from center of robot to face of AprilTag +TARGET_TO_ROBOT_TRANSLATION_Y = 0.0 # distance from center of robot to center of AprilTag (should be zero using alignment method above) +TARGET_TO_ROBOT_TRANSLATION_Z = -0.311 # distance from center of AprilTag to the floor (will be negative value as the robot is below the AprilTag) +TARGET_TO_ROBOT_ROTATION_YAW = 180.0 # rotation of robot to AprilTag (e.g. front of robot turned to face toward AprilTag is 180 degrees, rear of robot facing tag will be 0 degrees, sides will be -90/90 accordingly) + +TARGET_TO_CAMERA_TRANSLATION_X = 0.707 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics +TARGET_TO_CAMERA_TRANSLATION_Y = 0.041 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics +TARGET_TO_CAMERA_TRANSLATION_Z = -0.089 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics +TARGET_TO_CAMERA_QUATERNION_W = -0.01 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics +TARGET_TO_CAMERA_QUATERNION_X = -0.09 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics +TARGET_TO_CAMERA_QUATERNION_Y = 0.0 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics +TARGET_TO_CAMERA_QUATERNION_Z = -0.99 # calculated/average median value under targetPose reported by PhotonVision via AdvantageScope statistics + +# =========================================================================== + +targetToRobot = Transform3d( + Translation3d(TARGET_TO_ROBOT_TRANSLATION_X, TARGET_TO_ROBOT_TRANSLATION_Y, TARGET_TO_ROBOT_TRANSLATION_Z), + Rotation3d().fromDegrees(0, 0, TARGET_TO_ROBOT_ROTATION_YAW) +) + +targetToCamera = Transform3d( + Translation3d(TARGET_TO_CAMERA_TRANSLATION_X, TARGET_TO_CAMERA_TRANSLATION_Y, TARGET_TO_CAMERA_TRANSLATION_Z), + Rotation3d(Quaternion(TARGET_TO_CAMERA_QUATERNION_W, TARGET_TO_CAMERA_QUATERNION_X, TARGET_TO_CAMERA_QUATERNION_Y, TARGET_TO_CAMERA_QUATERNION_Z))) + +robotToCamera = Pose3d().transformBy(targetToCamera.inverse()) - Pose3d().transformBy(targetToRobot) + +constant = str(robotToCamera).replace("x=", "").replace("y=", "").replace("z=", "") + +print(constant) diff --git a/src/main/java/org/frc5010/lobbinloco/LobbinLoco.json b/src/main/resources/lobbinloco/LobbinLoco.json similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/LobbinLoco.json rename to src/main/resources/lobbinloco/LobbinLoco.json diff --git a/src/main/java/org/frc5010/lobbinloco/PathPlannerCustomField.png b/src/main/resources/lobbinloco/PathPlannerCustomField.png similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/PathPlannerCustomField.png rename to src/main/resources/lobbinloco/PathPlannerCustomField.png diff --git a/src/main/java/org/frc5010/lobbinloco/image.png b/src/main/resources/lobbinloco/image.png similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/image.png rename to src/main/resources/lobbinloco/image.png diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field2d_LobbinLoco/config.json b/src/main/resources/lobbinloco/userAssets/Field2d_LobbinLoco/config.json similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Field2d_LobbinLoco/config.json rename to src/main/resources/lobbinloco/userAssets/Field2d_LobbinLoco/config.json diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field2d_LobbinLoco/image.png b/src/main/resources/lobbinloco/userAssets/Field2d_LobbinLoco/image.png similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Field2d_LobbinLoco/image.png rename to src/main/resources/lobbinloco/userAssets/Field2d_LobbinLoco/image.png diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb b/src/main/resources/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb rename to src/main/resources/lobbinloco/userAssets/Field3d_LobbinLoco/LobbinLoco.glb diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/config.json b/src/main/resources/lobbinloco/userAssets/Field3d_LobbinLoco/config.json similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/config.json rename to src/main/resources/lobbinloco/userAssets/Field3d_LobbinLoco/config.json diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/model.glb b/src/main/resources/lobbinloco/userAssets/Field3d_LobbinLoco/model.glb similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Field3d_LobbinLoco/model.glb rename to src/main/resources/lobbinloco/userAssets/Field3d_LobbinLoco/model.glb diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Cart/config.json b/src/main/resources/lobbinloco/userAssets/Robot_Cart/config.json similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Cart/config.json rename to src/main/resources/lobbinloco/userAssets/Robot_Cart/config.json diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Cart/model.glb b/src/main/resources/lobbinloco/userAssets/Robot_Cart/model.glb similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Robot_Cart/model.glb rename to src/main/resources/lobbinloco/userAssets/Robot_Cart/model.glb diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_LobbinLoco/config.json b/src/main/resources/lobbinloco/userAssets/Robot_LobbinLoco/config.json similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Robot_LobbinLoco/config.json rename to src/main/resources/lobbinloco/userAssets/Robot_LobbinLoco/config.json diff --git a/src/main/java/org/frc5010/lobbinloco/userAssets/Robot_LobbinLoco/model.glb b/src/main/resources/lobbinloco/userAssets/Robot_LobbinLoco/model.glb similarity index 100% rename from src/main/java/org/frc5010/lobbinloco/userAssets/Robot_LobbinLoco/model.glb rename to src/main/resources/lobbinloco/userAssets/Robot_LobbinLoco/model.glb diff --git a/src/main/resources/schemas/akit_swerve_drivetrain.schema.json b/src/main/resources/schemas/akit_swerve_drivetrain.schema.json new file mode 100644 index 00000000..25cf6832 --- /dev/null +++ b/src/main/resources/schemas/akit_swerve_drivetrain.schema.json @@ -0,0 +1,418 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AKIT Swerve Drivetrain Configuration", + "description": "Configuration schema for AKIT swerve drivetrain setup. This schema enforces the structure required by AKitSwerveDrivetrainJson.java", + "type": "object", + "required": [ + "type", + "constants" + ], + "properties": { + "type": { + "type": "string", + "description": "The motor controller type. 'TalonFX' for TalonFX motors, 'SparkTalon' for Spark motors with Talon support, 'Spark' for NEO motors", + "enum": [ + "TalonFX", + "SparkTalon", + "Spark" + ] + }, + "constants": { + "type": "object", + "description": "Physical and control constants for the swerve drive", + "required": [ + "trackWidth", + "wheelBase", + "wheelDiameter", + "bumperFrameWidth", + "bumperFrameLength", + "maxDriveSpeed", + "gyro", + "driveGearRatio", + "steerGearRatio", + "driveMotorControl", + "steerMotorControl", + "modules", + "coupleRatio", + "invertLeftSide", + "invertRightSide", + "steerInertia", + "driveInertia", + "robotMass", + "wheelCOF", + "slipCurrent", + "canbus" + ], + "properties": { + "trackWidth": { + "$ref": "#/definitions/measurement", + "description": "Distance between left and right wheels" + }, + "wheelBase": { + "$ref": "#/definitions/measurement", + "description": "Distance between front and rear wheels" + }, + "wheelDiameter": { + "$ref": "#/definitions/measurement", + "description": "Diameter of the drive wheels" + }, + "bumperFrameWidth": { + "$ref": "#/definitions/measurement", + "description": "Width of the frame including bumpers" + }, + "bumperFrameLength": { + "$ref": "#/definitions/measurement", + "description": "Length of the frame including bumpers" + }, + "maxDriveSpeed": { + "$ref": "#/definitions/measurement", + "description": "Maximum drive speed of the robot" + }, + "gyro": { + "type": "object", + "description": "Gyroscope configuration", + "required": [ + "type", + "id", + "inverted", + "canbus" + ], + "properties": { + "type": { + "type": "string", + "description": "Type of gyroscope", + "enum": [ + "pigeon2", + "pigeon1", + "navx", + "adxrs450" + ] + }, + "id": { + "type": "integer", + "description": "CAN ID of the gyro", + "minimum": 0, + "maximum": 62 + }, + "inverted": { + "type": "boolean", + "description": "Whether the gyro is inverted" + }, + "canbus": { + "type": "string", + "description": "CAN bus name (e.g., 'canivore', 'rio')" + } + } + }, + "driveGearRatio": { + "type": "string", + "description": "Drive motor gear ratio (e.g., '1:7.363636363636365')" + }, + "steerGearRatio": { + "type": "string", + "description": "Steering motor gear ratio (e.g., '1:15.42857142857143')" + }, + "driveMotorControl": { + "$ref": "#/definitions/motorControl", + "description": "PID and feedforward control parameters for drive motors" + }, + "steerMotorControl": { + "$ref": "#/definitions/motorControl", + "description": "PID and feedforward control parameters for steering motors" + }, + "modules": { + "type": "object", + "description": "Configuration for each swerve module", + "required": [ + "frontLeft", + "frontRight", + "backLeft", + "backRight" + ], + "properties": { + "frontLeft": { + "$ref": "#/definitions/swerveModule", + "description": "Front-left swerve module" + }, + "frontRight": { + "$ref": "#/definitions/swerveModule", + "description": "Front-right swerve module" + }, + "backLeft": { + "$ref": "#/definitions/swerveModule", + "description": "Back-left swerve module" + }, + "backRight": { + "$ref": "#/definitions/swerveModule", + "description": "Back-right swerve module" + } + } + }, + "coupleRatio": { + "type": "number", + "description": "Couple ratio for the drivetrain" + }, + "invertLeftSide": { + "type": "boolean", + "description": "Whether to invert the left side motors" + }, + "invertRightSide": { + "type": "boolean", + "description": "Whether to invert the right side motors" + }, + "steerInertia": { + "$ref": "#/definitions/measurement", + "description": "Moment of inertia for steering motors" + }, + "driveInertia": { + "$ref": "#/definitions/measurement", + "description": "Moment of inertia for drive motors" + }, + "robotMass": { + "$ref": "#/definitions/measurement", + "description": "Total mass of the robot" + }, + "wheelCOF": { + "type": "number", + "description": "Wheel coefficient of friction" + }, + "slipCurrent": { + "$ref": "#/definitions/measurement", + "description": "Current threshold for wheel slip detection" + }, + "canbus": { + "type": "string", + "description": "Default CAN bus name for all devices" + } + } + } + }, + "definitions": { + "measurement": { + "type": "object", + "description": "A measurement with value and unit of measurement", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric value" + }, + "uom": { + "type": "string", + "description": "Unit of measurement (e.g., 'inches', 'meters', 'kg', 'amps', 'rotations', 'm/sec', 'kg*m^2')" + } + } + }, + "motorControl": { + "type": "object", + "description": "Motor control parameters including PID feedback and feedforward (matches MotorSystemIdJson)", + "properties": { + "closedLoopRamp": { + "$ref": "#/definitions/measurement", + "description": "Closed loop ramp time" + }, + "openLoopRamp": { + "$ref": "#/definitions/measurement", + "description": "Open loop ramp time" + }, + "feedBack": { + "type": "object", + "description": "PID feedback constants", + "properties": { + "p": { + "type": "number", + "description": "Proportional gain" + }, + "i": { + "type": "number", + "description": "Integral gain" + }, + "d": { + "type": "number", + "description": "Derivative gain" + } + } + }, + "maxVelocity": { + "$ref": "#/definitions/measurement", + "description": "Maximum velocity" + }, + "maxAcceleration": { + "$ref": "#/definitions/measurement", + "description": "Maximum acceleration" + }, + "feedForward": { + "type": "object", + "description": "Feedforward constants (static, gravity, velocity, acceleration)", + "properties": { + "s": { + "type": "number", + "description": "Static feedforward constant" + }, + "g": { + "type": "number", + "description": "Gravity feedforward constant" + }, + "v": { + "type": "number", + "description": "Velocity feedforward constant" + }, + "a": { + "type": "number", + "description": "Acceleration feedforward constant" + } + } + }, + "controlMode": { + "type": "string", + "description": "Control mode (e.g., CLOSED_LOOP, OPEN_LOOP)", + "enum": [ + "CLOSED_LOOP", + "OPEN_LOOP" + ] + } + } + }, + "motorSetup": { + "type": "object", + "description": "Motor configuration (matches MotorSetupJson)", + "required": [ + "name", + "controllerType", + "motorType", + "canId" + ], + "properties": { + "name": { + "type": "string", + "description": "Name identifier for the motor" + }, + "logLevel": { + "type": "string", + "description": "Logging level for the motor", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] + }, + "motorType": { + "type": "string", + "description": "Type of motor (e.g., KrakenX60, Falcon500, NEO)" + }, + "controllerType": { + "type": "string", + "description": "Type of motor controller", + "enum": [ + "talonfx", + "spark" + ] + }, + "canId": { + "type": "integer", + "description": "CAN ID of the motor", + "minimum": 0, + "maximum": 62 + }, + "canBus": { + "type": "string", + "description": "CAN bus name for the motor (e.g., 'canivore', 'rio')" + }, + "idleMode": { + "type": "string", + "description": "Idle mode for the motor", + "enum": [ + "BRAKE", + "COAST" + ] + }, + "currentLimit": { + "$ref": "#/definitions/measurement", + "description": "Current limit for the motor" + }, + "inverted": { + "type": "boolean", + "description": "Whether the motor is inverted" + }, + "numberOfMotors": { + "type": "integer", + "description": "Number of motors (including followers)", + "minimum": 1 + }, + "followers": { + "type": "array", + "description": "Follower motor configurations", + "items": { + "type": "object", + "properties": { + "canId": { + "type": "integer", + "description": "CAN ID of the follower motor", + "minimum": 0, + "maximum": 62 + }, + "inverted": { + "type": "boolean", + "description": "Whether the follower motor is inverted" + } + }, + "required": [ + "canId" + ] + } + }, + "robotToMotor": { + "type": "object", + "description": "Translation from robot center to motor" + }, + "movementPlane": { + "type": "string", + "description": "Movement plane for the motor", + "enum": [ + "XY", + "XZ", + "YZ" + ] + } + } + }, + "swerveModule": { + "type": "object", + "description": "Configuration for a single swerve module", + "required": [ + "driveMotorSetup", + "steerMotorSetup", + "encoderId", + "absoluteOffset", + "encoderInverted" + ], + "properties": { + "driveMotorSetup": { + "$ref": "#/definitions/motorSetup", + "description": "Drive motor configuration" + }, + "steerMotorSetup": { + "$ref": "#/definitions/motorSetup", + "description": "Steering motor configuration" + }, + "encoderId": { + "type": "integer", + "description": "CAN ID or index of the absolute encoder", + "minimum": 0, + "maximum": 63 + }, + "absoluteOffset": { + "$ref": "#/definitions/measurement", + "description": "Absolute encoder offset (typically in rotations or degrees)" + }, + "encoderInverted": { + "type": "boolean", + "description": "Whether the encoder is inverted" + } + } + } + } +} diff --git a/src/main/resources/schemas/angle-unit-value.schema.json b/src/main/resources/schemas/angle-unit-value.schema.json new file mode 100644 index 00000000..662874f7 --- /dev/null +++ b/src/main/resources/schemas/angle-unit-value.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Angle Unit Value", + "description": "A numeric angle value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric angle value" + }, + "uom": { + "type": "string", + "enum": [ + "deg", + "degrees", + "rad", + "radians", + "rot", + "rotation", + "rotations" + ], + "description": "Angle unit (degrees, radians, or rotations)" + } + } +} diff --git a/src/main/resources/schemas/angular-acceleration-unit-value.schema.json b/src/main/resources/schemas/angular-acceleration-unit-value.schema.json new file mode 100644 index 00000000..18c7cb8d --- /dev/null +++ b/src/main/resources/schemas/angular-acceleration-unit-value.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Angular Acceleration Unit Value", + "description": "A numeric angular acceleration value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric angular acceleration value" + }, + "uom": { + "type": "string", + "enum": [ + "deg/s^2", + "deg/s/s", + "deg/s2", + "degrees/s/s", + "degrees/s2", + "degrees/s^2", + "rad/s^2", + "rads/s/s", + "rads/s2", + "rads/s^2", + "radians", + "radians/s/s", + "radians/s2", + "radians/s^2", + "rpm/s", + "RPM/s", + "rpm/sec", + "RPM/sec", + "rps^2", + "RPS^2", + "RPS2", + "RPS/s", + "RPS/sec", + "rps2", + "rps/s", + "rps/sec" + ], + "description": "Angular acceleration unit (deg/s^2, rad/s^2, rpm/s, or rps^2)" + } + } +} diff --git a/src/main/resources/schemas/angular-velocity-unit-value.schema.json b/src/main/resources/schemas/angular-velocity-unit-value.schema.json new file mode 100644 index 00000000..d197522a --- /dev/null +++ b/src/main/resources/schemas/angular-velocity-unit-value.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Angular Velocity Unit Value", + "description": "A numeric angular velocity value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric angular velocity value" + }, + "uom": { + "type": "string", + "enum": [ + "deg/s", + "deg/sec", + "deg/second", + "degrees/s", + "degrees/sec", + "degrees/second", + "rad/s", + "rad/sec", + "rad/second", + "rads/s", + "rads/sec", + "rads/second", + "radians", + "radians/s", + "radians/sec", + "radians/second", + "rpm", + "RPM", + "rps", + "RPS" + ], + "description": "Angular velocity unit (deg/s, rad/s, rpm, or rps)" + } + } +} diff --git a/src/main/resources/schemas/camera.schema.json b/src/main/resources/schemas/camera.schema.json new file mode 100644 index 00000000..c8e56483 --- /dev/null +++ b/src/main/resources/schemas/camera.schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Camera Configuration", + "description": "Configuration for a single camera (matches CameraConfigurationJson.java)", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique name of the camera used as subsystem identifier" + }, + "use": { + "type": "string", + "description": "Use case for this camera", + "enum": [ + "target", + "apriltag", + "quest" + ] + }, + "type": { + "type": "string", + "description": "Camera vendor/type", + "enum": [ + "limelight", + "photonvision", + "none" + ], + "default": "none" + }, + "strategy": { + "type": "string", + "description": "Pose estimation strategy for PhotonVision cameras", + "default": "none" + }, + "column": { + "type": "integer", + "description": "SmartDashboard column index for telemetry display", + "default": 0, + "minimum": 0 + }, + "x": { + "type": "number", + "description": "Camera X position offset from robot center in meters (forward/backward)", + "default": 0 + }, + "y": { + "type": "number", + "description": "Camera Y position offset from robot center in meters (left/right)", + "default": 0 + }, + "z": { + "type": "number", + "description": "Camera Z position offset from robot center in meters (up/down)", + "default": 0 + }, + "roll": { + "type": "number", + "description": "Camera roll rotation in degrees around X-axis", + "default": 0 + }, + "pitch": { + "type": "number", + "description": "Camera pitch rotation in degrees around Y-axis", + "default": 0 + }, + "yaw": { + "type": "number", + "description": "Camera yaw rotation in degrees around Z-axis", + "default": 0 + }, + "width": { + "type": "integer", + "description": "Horizontal resolution of the camera in pixels", + "default": 800, + "minimum": 1 + }, + "height": { + "type": "integer", + "description": "Vertical resolution of the camera in pixels", + "default": 600, + "minimum": 1 + } + } +} diff --git a/src/main/resources/schemas/cameras.schema.json b/src/main/resources/schemas/cameras.schema.json new file mode 100644 index 00000000..7639cfb4 --- /dev/null +++ b/src/main/resources/schemas/cameras.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cameras Configuration", + "description": "List of camera configuration files (matches CameraConfigurationJson.java)", + "type": "object", + "required": [ + "cameras" + ], + "properties": { + "cameras": { + "type": "array", + "description": "Array of camera configuration file names in the cameras/ subdirectory", + "items": { + "type": "string", + "description": "Camera configuration file name (e.g., 'intake.json')" + }, + "minItems": 0 + } + } +} diff --git a/src/main/resources/schemas/controllers.schema.json b/src/main/resources/schemas/controllers.schema.json new file mode 100644 index 00000000..3b8ae5f0 --- /dev/null +++ b/src/main/resources/schemas/controllers.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Controllers Configuration", + "description": "List of driveteam controller configuration files (matches DriveteamControllersJson.java)", + "type": "object", + "required": [ + "controllers" + ], + "properties": { + "controllers": { + "type": "array", + "description": "Array of controller configuration file names in the controllers/ subdirectory", + "items": { + "type": "string", + "description": "Controller configuration file name (e.g., 'driver.json', 'operator.json')" + }, + "minItems": 1 + } + } +} diff --git a/src/main/resources/schemas/current-unit-value.schema.json b/src/main/resources/schemas/current-unit-value.schema.json new file mode 100644 index 00000000..939ece1a --- /dev/null +++ b/src/main/resources/schemas/current-unit-value.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Current Unit Value", + "description": "A numeric current value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric current value" + }, + "uom": { + "type": "string", + "enum": [ + "a", + "amp", + "amps", + "ampere", + "amperes", + "ma", + "milliamp", + "milliamps", + "milliampere", + "milliamperes", + "ua", + "microamp", + "microamps", + "microampere", + "microamperes" + ], + "description": "Current unit (amps, milliamps, or microamps)" + } + } +} diff --git a/src/main/resources/schemas/distance-unit-value.schema.json b/src/main/resources/schemas/distance-unit-value.schema.json new file mode 100644 index 00000000..11477d34 --- /dev/null +++ b/src/main/resources/schemas/distance-unit-value.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Distance Unit Value", + "description": "A numeric distance value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric distance value" + }, + "uom": { + "type": "string", + "enum": [ + "m", + "meter", + "meters", + "in", + "inch", + "inches", + "ft", + "foot", + "feet", + "mm", + "millimeter", + "millimeters", + "cm", + "centimeter", + "centimeters", + "yd", + "yard", + "yards" + ], + "description": "Distance unit (meters, inches, feet, mm, cm, or yards)" + } + } +} diff --git a/src/main/resources/schemas/driveteam-controller-axis.schema.json b/src/main/resources/schemas/driveteam-controller-axis.schema.json new file mode 100644 index 00000000..9a6d49ae --- /dev/null +++ b/src/main/resources/schemas/driveteam-controller-axis.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Controller Axis Configuration", + "description": "Configuration for a single controller axis (matches DriveteamControllerAxisJson.java)", + "type": "object", + "required": [ + "channel" + ], + "properties": { + "channel": { + "type": "integer", + "description": "Channel number of the axis on the controller (0-5 for analog, 0-11 for buttons)", + "minimum": 0, + "maximum": 11 + }, + "deadband": { + "type": "number", + "description": "Minimum value threshold for the axis to register motion (0.0-1.0)", + "default": 0.0, + "minimum": 0.0, + "maximum": 1.0 + }, + "invert": { + "type": "boolean", + "description": "Whether the axis should be inverted", + "default": false + }, + "scale": { + "type": "number", + "description": "Scaling factor for the axis output", + "default": 1.0, + "minimum": -10, + "maximum": 10 + }, + "curvePower": { + "type": "number", + "description": "Mathematical power to apply for curved axis response (1.0 = linear)", + "default": 1.0, + "minimum": 0.1, + "maximum": 10.0 + }, + "limit": { + "type": "number", + "description": "Absolute maximum limit value for the axis (0.0-1.0)", + "default": 1.0, + "minimum": 0.0, + "maximum": 1.0 + }, + "rate": { + "type": "number", + "description": "Slew rate limiting for the axis (0.0 = no limit)", + "default": 0.0, + "minimum": 0.0 + } + } +} diff --git a/src/main/resources/schemas/driveteam-controller.schema.json b/src/main/resources/schemas/driveteam-controller.schema.json new file mode 100644 index 00000000..aedaac3d --- /dev/null +++ b/src/main/resources/schemas/driveteam-controller.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Driveteam Controller Configuration", + "description": "Configuration for a single driveteam controller (matches DriveteamControllerJson.java)", + "type": "object", + "required": [ + "name", + "port", + "axis" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the driveteam controller (e.g., 'driver', 'operator')" + }, + "port": { + "type": "integer", + "description": "USB port number of the controller (0-5 typically)", + "minimum": 0, + "maximum": 5 + }, + "axis": { + "type": "array", + "description": "Array of axis configuration file names in the controllers/axis/ subdirectory", + "items": { + "type": "string", + "description": "Axis configuration file name" + }, + "minItems": 1 + } + } +} diff --git a/src/main/resources/schemas/led-strip.schema.json b/src/main/resources/schemas/led-strip.schema.json new file mode 100644 index 00000000..11326cae --- /dev/null +++ b/src/main/resources/schemas/led-strip.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LED Strip Configuration", + "description": "Configuration for addressable LED strips (matches LEDStripConfigJson.java)", + "type": "object", + "required": [ + "length", + "dataPin" + ], + "properties": { + "length": { + "type": "integer", + "description": "Total number of LEDs in the strip", + "minimum": 1 + }, + "dataPin": { + "type": "integer", + "description": "PWM pin number for the LED data line", + "minimum": 0 + }, + "segments": { + "type": "array", + "description": "Array of LED segments with names and ranges", + "items": { + "$ref": "#/definitions/ledSegment" + }, + "minItems": 0 + } + }, + "definitions": { + "ledSegment": { + "type": "object", + "title": "LED Segment", + "description": "A named segment of LEDs for control purposes", + "required": [ + "name", + "start", + "end" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the LED segment (e.g., 'status_indicator', 'team_colors')" + }, + "start": { + "type": "integer", + "description": "Index of the first LED in this segment", + "minimum": 0 + }, + "end": { + "type": "integer", + "description": "Index of the last LED in this segment (inclusive)", + "minimum": 0 + }, + "order": { + "type": "integer", + "description": "Display order of this segment", + "default": 1, + "minimum": 1 + } + } + } + } +} diff --git a/src/main/resources/schemas/linear-acceleration-unit-value.schema.json b/src/main/resources/schemas/linear-acceleration-unit-value.schema.json new file mode 100644 index 00000000..5725157a --- /dev/null +++ b/src/main/resources/schemas/linear-acceleration-unit-value.schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Linear Acceleration Unit Value", + "description": "A numeric linear acceleration value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric linear acceleration value" + }, + "uom": { + "type": "string", + "enum": [ + "m/s^2", + "m/s/s", + "m/s2", + "m/sec/sec", + "m/sec2", + "m/sec^2", + "meters/sec/sec", + "meters/sec2", + "meters/sec^2", + "meters/second/second", + "meters/second2", + "meters/second^2", + "in/s^2", + "in/s/s", + "in/s2", + "in/sec/sec", + "in/sec2", + "in/sec^2", + "inches/sec/sec", + "inches/sec2", + "inches/sec^2", + "inches/second/second", + "inches/second2", + "inches/second^2", + "ft/s^2", + "ft/s/s", + "ft/s2", + "ft/sec/sec", + "ft/sec2", + "ft/sec^2", + "feet/sec/sec", + "feet/sec2", + "feet/sec^2", + "feet/second/second", + "feet/second2", + "feet/second^2", + "mm/s^2", + "mm/s/s", + "mm/s2", + "millimeter/sec/sec", + "millimeter/sec2", + "millimeter/sec^2", + "millimeters/sec/sec", + "millimeters/sec2", + "millimeters/sec^2", + "millimeters/second/second", + "millimeters/second2", + "millimeters/second^2", + "cm/s^2", + "cm/sec^2", + "cm/second^2", + "centimeter/sec^2", + "centimeter/second^2", + "centimeters/sec^2", + "centimeters/second^2", + "cms/s^2", + "cms/sec^2", + "cms/second^2", + "cm/s/s", + "cm/sec/sec", + "cm/second/second", + "centimeters/sec/sec", + "centimeters/second/second" + ], + "description": "Linear acceleration unit (m/s^2, in/s^2, ft/s^2, mm/s^2, or cm/s^2)" + } + } +} diff --git a/src/main/resources/schemas/linear-velocity-unit-value.schema.json b/src/main/resources/schemas/linear-velocity-unit-value.schema.json new file mode 100644 index 00000000..70d16820 --- /dev/null +++ b/src/main/resources/schemas/linear-velocity-unit-value.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Linear Velocity Unit Value", + "description": "A numeric linear velocity value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric linear velocity value" + }, + "uom": { + "type": "string", + "enum": [ + "m/s", + "m/sec", + "mps", + "meter/sec", + "meters/sec", + "meter/second", + "meters/second", + "in/s", + "in/sec", + "inch/sec", + "inches/sec", + "inch/second", + "inches/second", + "ft/s", + "ft/sec", + "foot/sec", + "feet/sec", + "foot/second", + "feet/second", + "mm/s", + "mm/sec", + "millimeter/sec", + "millimeters/sec", + "millimeter/second", + "millimeters/second", + "cm/s", + "cm/sec", + "centimeter/sec", + "centimeters/sec", + "centimeter/second", + "centimeters/second" + ], + "description": "Linear velocity unit (m/s, in/s, ft/s, mm/s, or cm/s)" + } + } +} diff --git a/src/main/resources/schemas/mass-unit-value.schema.json b/src/main/resources/schemas/mass-unit-value.schema.json new file mode 100644 index 00000000..0e3dab55 --- /dev/null +++ b/src/main/resources/schemas/mass-unit-value.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Mass Unit Value", + "description": "A numeric mass value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric mass value" + }, + "uom": { + "type": "string", + "enum": [ + "kg", + "kgs", + "kilogram", + "kilograms", + "g", + "gram", + "grams", + "mg", + "milligram", + "milligrams", + "oz", + "ounce", + "ounces", + "lbs", + "lb", + "pound", + "pounds", + "st", + "stone", + "stones", + "t", + "tons", + "ton" + ], + "description": "Mass unit (kg, g, mg, oz, lbs, stone, or tons)" + } + } +} diff --git a/src/main/resources/schemas/motor-setup.schema.json b/src/main/resources/schemas/motor-setup.schema.json new file mode 100644 index 00000000..b31f13af --- /dev/null +++ b/src/main/resources/schemas/motor-setup.schema.json @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Motor Setup", + "description": "Motor controller and hardware configuration (matches MotorSetupJson.java)", + "type": "object", + "required": [ + "name", + "controllerType", + "motorType", + "canId" + ], + "properties": { + "name": { + "type": "string", + "description": "Name identifier for the motor" + }, + "logLevel": { + "type": "string", + "description": "Logging/telemetry level", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ], + "default": "LOW" + }, + "motorType": { + "type": "string", + "description": "Type of motor (e.g., KrakenX60, Neo, Falcon500)" + }, + "controllerType": { + "type": "string", + "description": "Type of motor controller", + "enum": [ + "talonfx", + "talonsrx", + "sparkmax", + "spark" + ] + }, + "canId": { + "type": "integer", + "description": "CAN ID of the motor", + "minimum": 0, + "maximum": 62 + }, + "canBus": { + "type": "string", + "description": "CAN bus name for the motor", + "default": "" + }, + "idleMode": { + "type": "string", + "description": "Idle mode for the motor", + "enum": [ + "BRAKE", + "COAST" + ], + "default": "BRAKE" + }, + "currentLimit": { + "$ref": "./unit-value.schema.json", + "description": "Current limit for the motor" + }, + "inverted": { + "type": "boolean", + "description": "Whether the motor is inverted", + "default": false + }, + "numberOfMotors": { + "type": "integer", + "description": "Number of motors (including followers)", + "default": 1, + "minimum": 1 + }, + "followers": { + "type": "array", + "description": "Follower motor configurations", + "items": { + "type": "object", + "properties": { + "canId": { + "type": "integer", + "description": "CAN ID of the follower motor", + "minimum": 0, + "maximum": 62 + }, + "inverted": { + "type": "boolean", + "description": "Whether the follower motor is inverted" + } + }, + "required": [ + "canId" + ] + } + } + } +} diff --git a/src/main/resources/schemas/motor-system-id.schema.json b/src/main/resources/schemas/motor-system-id.schema.json new file mode 100644 index 00000000..a54ad9af --- /dev/null +++ b/src/main/resources/schemas/motor-system-id.schema.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Motor System ID", + "description": "PID and feedforward control parameters for a motor (matches MotorSystemIdJson.java)", + "type": "object", + "properties": { + "closedLoopRamp": { + "$ref": "./unit-value.schema.json", + "description": "Closed loop ramp time" + }, + "openLoopRamp": { + "$ref": "./unit-value.schema.json", + "description": "Open loop ramp time" + }, + "feedBack": { + "type": "object", + "description": "PID feedback constants", + "properties": { + "p": { + "type": "number", + "description": "Proportional gain", + "default": 0 + }, + "i": { + "type": "number", + "description": "Integral gain", + "default": 0 + }, + "d": { + "type": "number", + "description": "Derivative gain", + "default": 0 + } + } + }, + "maxVelocity": { + "$ref": "./unit-value.schema.json", + "description": "Maximum velocity setpoint" + }, + "maxAcceleration": { + "$ref": "./unit-value.schema.json", + "description": "Maximum acceleration setpoint" + }, + "feedForward": { + "type": "object", + "description": "Feedforward constants (s, g, v, a)", + "properties": { + "s": { + "type": "number", + "description": "Static feedforward constant", + "default": 0 + }, + "g": { + "type": "number", + "description": "Gravity feedforward constant (for arms/pivots)", + "default": 0 + }, + "v": { + "type": "number", + "description": "Velocity feedforward constant", + "default": 0 + }, + "a": { + "type": "number", + "description": "Acceleration feedforward constant", + "default": 0 + } + } + }, + "controlMode": { + "type": "string", + "description": "Control mode for the motor", + "enum": [ + "CLOSED_LOOP", + "OPEN_LOOP" + ], + "default": "CLOSED_LOOP" + } + } +} diff --git a/src/main/resources/schemas/percent-motor.schema.json b/src/main/resources/schemas/percent-motor.schema.json new file mode 100644 index 00000000..1be92720 --- /dev/null +++ b/src/main/resources/schemas/percent-motor.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Percent Motor Configuration", + "description": "Configuration for a simple percent-output motor (matches PercentMotorConfigurationJson.java)", + "type": "object", + "required": [ + "name", + "controller", + "type", + "id" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the motor subsystem" + }, + "controller": { + "type": "string", + "description": "Type of motor controller", + "enum": [ + "spark", + "talonfx", + "talonsrx", + "sparkmax" + ] + }, + "type": { + "type": "string", + "description": "Type of motor (e.g., Neo, KrakenX60, Falcon500)" + }, + "id": { + "type": "integer", + "description": "CAN ID of the motor", + "minimum": 0, + "maximum": 62 + }, + "gearing": { + "type": "number", + "description": "Motor gearing/reduction ratio", + "default": 1.0, + "minimum": 0 + }, + "momentOfInertiaKgMSq": { + "type": "number", + "description": "Moment of inertia for simulation in kg⋅m²", + "default": 1.0, + "minimum": 0 + }, + "x": { + "type": "number", + "description": "X position of motor for visualization (meters)", + "default": 0 + }, + "y": { + "type": "number", + "description": "Y position of motor for visualization (meters)", + "default": 0 + }, + "z": { + "type": "number", + "description": "Z position of motor for visualization (meters)", + "default": 0 + }, + "logLevel": { + "type": "string", + "description": "Logging level for telemetry", + "enum": [ + "COMPETITION", + "TESTING", + "DEBUG" + ], + "default": "COMPETITION" + } + } +} diff --git a/src/main/resources/schemas/robot.schema.json b/src/main/resources/schemas/robot.schema.json new file mode 100644 index 00000000..2417d58f --- /dev/null +++ b/src/main/resources/schemas/robot.schema.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Robot Configuration", + "description": "Main robot configuration file (matches RobotJson.java)", + "type": "object", + "properties": { + "userConfig": { + "type": "string", + "description": "User configuration file for the power/operation mode (e.g., competition_mode.json, demo_mode.json)", + "default": "competition_mode.json" + }, + "driveType": { + "type": "string", + "description": "Type of drivetrain configuration", + "enum": [ + "YAGSL_SWERVE_DRIVE", + "AKIT_SWERVE_DRIVE" + ] + }, + "trackWidth": { + "type": "number", + "description": "Drivetrain width between wheel centers", + "default": 0.0, + "minimum": 0 + }, + "trackWidthUom": { + "type": "string", + "description": "Unit of measure for trackWidth", + "default": "m", + "enum": [ + "m", + "cm", + "mm", + "in", + "ft" + ] + }, + "wheelBase": { + "type": "number", + "description": "Drivetrain length between wheel centers", + "default": 0.0, + "minimum": 0 + }, + "wheelBaseUom": { + "type": "string", + "description": "Unit of measure for wheelBase", + "default": "m", + "enum": [ + "m", + "cm", + "mm", + "in", + "ft" + ] + }, + "wheelDiameter": { + "type": "number", + "description": "Drivetrain wheel diameter", + "default": 0.0, + "minimum": 0 + }, + "wheelDiameterUom": { + "type": "string", + "description": "Unit of measure for wheelDiameter", + "default": "m", + "enum": [ + "m", + "cm", + "mm", + "in", + "ft" + ] + }, + "physicalMaxSpeed": { + "type": "number", + "description": "Drivetrain physical maximum speed capability", + "default": 0.0, + "minimum": 0 + }, + "physicalMaxSpeedUom": { + "type": "string", + "description": "Unit of measure for physicalMaxSpeed", + "default": "m/s", + "enum": [ + "m/s", + "ft/s", + "in/s" + ] + }, + "driveMotorGearRatio": { + "type": "number", + "description": "Gear ratio between drive motor and wheels", + "default": 1.0, + "minimum": 0 + }, + "loadSimulatedField": { + "type": "boolean", + "description": "Whether to load the simulated field in simulation mode", + "default": true + }, + "gamePieceA": { + "type": "string", + "description": "Name of the first game piece type", + "default": "GPA" + }, + "gamePieceB": { + "type": "string", + "description": "Name of the second game piece type", + "default": "GPB" + } + } +} diff --git a/src/main/resources/schemas/robots.schema.json b/src/main/resources/schemas/robots.schema.json new file mode 100644 index 00000000..18e4d9db --- /dev/null +++ b/src/main/resources/schemas/robots.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Robots Configuration", + "description": "Configuration schema for robots.json. This schema enforces the structure required by RobotsJson.java", + "type": "object", + "required": [ + "robots" + ], + "properties": { + "competitionPin": { + "type": "integer", + "description": "DIO pin number for competition robot selector switch. Use -1 to disable. Negative value disables competition mode.", + "default": -1 + }, + "robots": { + "type": "object", + "description": "Map of robot configurations, keyed by robot directory name (e.g., 'basic_robot', 'baby_swerve')", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/definitions/robotIdentity" + } + } + }, + "definitions": { + "robotIdentity": { + "type": "object", + "title": "Robot Identity Configuration", + "description": "Configuration for a single robot identity (matches RobotIdentity.java)", + "required": [ + "id", + "robotClass" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the robot. Can be a MAC address (format: XX:XX:XX:XX:XX:XX) or any unique string identifier" + }, + "robotClass": { + "type": "string", + "description": "Fully qualified Java class name for the robot (must extend GenericRobot). Example: 'frc.robot.example.ExampleRobot'" + }, + "simulate": { + "type": "boolean", + "description": "Whether this robot should be used in simulation mode", + "default": false + }, + "competition": { + "type": "boolean", + "description": "Whether this robot should be used when competition pin is active", + "default": false + } + }, + "examples": [ + { + "id": "basic_robot", + "robotClass": "frc.robot.example.ExampleRobot", + "simulate": true, + "competition": false + }, + { + "id": "00:80:2F:24:6D:74", + "robotClass": "frc.robot.baby_swerve.BabySwerve", + "simulate": true, + "competition": true + } + ] + } + } +} diff --git a/src/main/resources/schemas/time-unit-value.schema.json b/src/main/resources/schemas/time-unit-value.schema.json new file mode 100644 index 00000000..61209e3a --- /dev/null +++ b/src/main/resources/schemas/time-unit-value.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Time Unit Value", + "description": "A numeric time value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric time value" + }, + "uom": { + "type": "string", + "enum": [ + "s", + "sec", + "second", + "seconds", + "ms", + "millisecond", + "milliseconds", + "us", + "microsecond", + "microseconds", + "ns", + "nanosecond", + "nanoseconds", + "min", + "minute", + "minutes", + "h", + "hour", + "hours", + "d", + "day", + "days" + ], + "description": "Time unit (seconds, milliseconds, microseconds, nanoseconds, minutes, hours, or days)" + } + } +} diff --git a/src/main/resources/schemas/unit-value.schema.json b/src/main/resources/schemas/unit-value.schema.json new file mode 100644 index 00000000..cf05ea63 --- /dev/null +++ b/src/main/resources/schemas/unit-value.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Unit Value", + "description": "A numeric value with a unit of measure (matches UnitValueJson.java)", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric value" + }, + "uom": { + "type": "string", + "description": "Unit of measure (e.g., 'inches', 'm', 'deg', 'rpm', 'rad/s', 'kg', 'lbs', 'amps', 'volts')" + } + } +} diff --git a/src/main/resources/schemas/user-mode.schema.json b/src/main/resources/schemas/user-mode.schema.json new file mode 100644 index 00000000..f6ee413b --- /dev/null +++ b/src/main/resources/schemas/user-mode.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User Mode Configuration", + "description": "User mode settings for robot operation (competition_mode.json, demo_mode.json, etc. - matches UserModeJson.java)", + "type": "object", + "properties": { + "maxSpeed": { + "type": "number", + "description": "Maximum teleop speed of the robot in m/s", + "default": 0.0, + "minimum": 0 + }, + "maxAngularSpeed": { + "type": "number", + "description": "Maximum angular speed of the robot in rad/s", + "default": 0.0, + "minimum": 0 + }, + "maxAccelleration": { + "type": "number", + "description": "Maximum linear acceleration in m/s²", + "default": 0.0, + "minimum": 0 + }, + "maxAngularAccelleration": { + "type": "number", + "description": "Maximum angular acceleration in rad/s²", + "default": 0.0, + "minimum": 0 + } + } +} diff --git a/src/main/resources/schemas/velocity-motor.schema.json b/src/main/resources/schemas/velocity-motor.schema.json new file mode 100644 index 00000000..c86d1f31 --- /dev/null +++ b/src/main/resources/schemas/velocity-motor.schema.json @@ -0,0 +1,111 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Velocity Motor Configuration", + "description": "Configuration for a velocity-controlled motor with PID and feedforward (matches VelocityMotorConfigurationJson.java)", + "type": "object", + "required": [ + "name", + "controller", + "type", + "id" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the motor subsystem" + }, + "controller": { + "type": "string", + "description": "Type of motor controller", + "enum": [ + "spark", + "talonfx", + "talonsrx", + "sparkmax" + ] + }, + "type": { + "type": "string", + "description": "Type of motor (e.g., Neo, KrakenX60, Falcon500)" + }, + "id": { + "type": "integer", + "description": "CAN ID of the motor", + "minimum": 0, + "maximum": 62 + }, + "gearing": { + "type": "number", + "description": "Motor gearing/reduction ratio", + "default": 1.0, + "minimum": 0 + }, + "momentOfInertiaKgMSq": { + "type": "number", + "description": "Moment of inertia for simulation in kg⋅m²", + "default": 1.0, + "minimum": 0 + }, + "x": { + "type": "number", + "description": "X position of motor for visualization (meters)", + "default": 0 + }, + "y": { + "type": "number", + "description": "Y position of motor for visualization (meters)", + "default": 0 + }, + "z": { + "type": "number", + "description": "Z position of motor for visualization (meters)", + "default": 0 + }, + "kP": { + "type": "number", + "description": "PID Proportional gain", + "default": 0.0 + }, + "kI": { + "type": "number", + "description": "PID Integral gain", + "default": 0.0 + }, + "kD": { + "type": "number", + "description": "PID Derivative gain", + "default": 0.0 + }, + "iZone": { + "type": "number", + "description": "PID I-Zone threshold", + "default": 0.0, + "minimum": 0 + }, + "kS": { + "type": "number", + "description": "Feedforward static gain", + "default": 0.0 + }, + "kV": { + "type": "number", + "description": "Feedforward velocity gain", + "default": 0.0 + }, + "kA": { + "type": "number", + "description": "Feedforward acceleration gain", + "default": 0.0 + }, + "logLevel": { + "type": "string", + "description": "Logging level for telemetry", + "enum": [ + "COMPETITION", + "TESTING", + "DEBUG" + ], + "default": "COMPETITION" + } + } +} diff --git a/src/main/resources/schemas/voltage-unit-value.schema.json b/src/main/resources/schemas/voltage-unit-value.schema.json new file mode 100644 index 00000000..8857c976 --- /dev/null +++ b/src/main/resources/schemas/voltage-unit-value.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Voltage Unit Value", + "description": "A numeric voltage value with unit of measure", + "type": "object", + "required": [ + "val", + "uom" + ], + "properties": { + "val": { + "type": "number", + "description": "The numeric voltage value" + }, + "uom": { + "type": "string", + "enum": [ + "v", + "volt", + "volts", + "voltage", + "mv", + "millivolt", + "millivolts", + "uv", + "microvolt", + "microvolts", + "kv", + "kilovolt", + "kilovolts" + ], + "description": "Voltage unit (volts, millivolts, microvolts, or kilovolts)" + } + } +} diff --git a/src/main/resources/schemas/yams-arm.schema.json b/src/main/resources/schemas/yams-arm.schema.json new file mode 100644 index 00000000..42ba7318 --- /dev/null +++ b/src/main/resources/schemas/yams-arm.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "YAMS Arm Configuration", + "description": "Configuration for a YAMS robotic arm with angular positioning (matches YamsArmConfigurationJson.java)", + "type": "object", + "required": [ + "motorSetup", + "motorSystemId" + ], + "properties": { + "motorSetup": { + "$ref": "./motor-setup.schema.json", + "description": "Motor controller and hardware configuration" + }, + "motorSystemId": { + "$ref": "./motor-system-id.schema.json", + "description": "PID and feedforward control parameters" + }, + "length": { + "$ref": "./unit-value.schema.json", + "description": "Effective length of the arm" + }, + "lowerHardLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower hardware limit angle (degrees)" + }, + "upperHardLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper hardware limit angle (degrees)" + }, + "startingAngle": { + "$ref": "./unit-value.schema.json", + "description": "Initial arm angle at startup (degrees)" + }, + "lowerSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower software limit angle (degrees)" + }, + "upperSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper software limit angle (degrees)" + }, + "gearing": { + "type": "array", + "description": "Gear reduction stages (e.g., [3, 4] means 3:4 reduction)", + "items": { + "type": "number", + "minimum": 0 + } + }, + "mass": { + "$ref": "./unit-value.schema.json", + "description": "Mass of the arm payload" + }, + "voltageCompensation": { + "$ref": "./unit-value.schema.json", + "description": "Voltage compensation value for simulation" + }, + "horizontalZero": { + "$ref": "./unit-value.schema.json", + "description": "Angle at which arm is horizontal (degrees)" + } + } +} diff --git a/src/main/resources/schemas/yams-elevator.schema.json b/src/main/resources/schemas/yams-elevator.schema.json new file mode 100644 index 00000000..bb44895e --- /dev/null +++ b/src/main/resources/schemas/yams-elevator.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "YAMS Elevator Configuration", + "description": "Configuration for a YAMS elevator mechanism with linear positioning (matches YamsElevatorConfigurationJson.java)", + "type": "object", + "required": [ + "motorSetup", + "motorSystemId" + ], + "properties": { + "motorSetup": { + "$ref": "./motor-setup.schema.json", + "description": "Motor controller and hardware configuration" + }, + "motorSystemId": { + "$ref": "./motor-system-id.schema.json", + "description": "PID and feedforward control parameters" + }, + "sprocketTeeth": { + "type": "integer", + "description": "Number of teeth on sprocket for chain drive (specify either sprocketTeeth or drumRadius)", + "minimum": 0 + }, + "drumRadius": { + "$ref": "./unit-value.schema.json", + "description": "Drum radius for direct drive (specify either sprocketTeeth or drumRadius)" + }, + "lowerSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower software limit height (meters)" + }, + "upperSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper software limit height (meters)" + }, + "lowerHardLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower hardware limit height (meters)" + }, + "upperHardLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper hardware limit height (meters)" + }, + "gearing": { + "type": "array", + "description": "Gear reduction stages (e.g., [3, 4] means 3:4 reduction)", + "items": { + "type": "number", + "minimum": 0 + } + }, + "startingPosition": { + "$ref": "./unit-value.schema.json", + "description": "Initial elevator height at startup (meters)" + }, + "mass": { + "$ref": "./unit-value.schema.json", + "description": "Mass of the elevator carriage and payload" + }, + "voltageCompensation": { + "$ref": "./unit-value.schema.json", + "description": "Voltage compensation value for simulation" + } + } +} diff --git a/src/main/resources/schemas/yams-pivot.schema.json b/src/main/resources/schemas/yams-pivot.schema.json new file mode 100644 index 00000000..2bd954a9 --- /dev/null +++ b/src/main/resources/schemas/yams-pivot.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "YAMS Pivot Configuration", + "description": "Configuration for a YAMS pivot mechanism (turret/rotation) with angular positioning (matches YamsPivotConfigurationJson.java)", + "type": "object", + "required": [ + "motorSetup", + "motorSystemId" + ], + "properties": { + "motorSetup": { + "$ref": "./motor-setup.schema.json", + "description": "Motor controller and hardware configuration" + }, + "motorSystemId": { + "$ref": "./motor-system-id.schema.json", + "description": "PID and feedforward control parameters" + }, + "lowerHardLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower hardware limit angle (degrees)" + }, + "upperHardLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper hardware limit angle (degrees)" + }, + "startingAngle": { + "$ref": "./unit-value.schema.json", + "description": "Initial pivot angle at startup (degrees)" + }, + "lowerSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower software limit angle (degrees)" + }, + "upperSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper software limit angle (degrees)" + }, + "gearing": { + "type": "array", + "description": "Gear reduction stages (e.g., [3, 4] means 3:4 reduction)", + "items": { + "type": "number", + "minimum": 0 + } + }, + "voltageCompensation": { + "$ref": "./unit-value.schema.json", + "description": "Voltage compensation value for simulation" + }, + "startingPosition": { + "$ref": "./unit-value.schema.json", + "description": "Initial pivot angle at startup (degrees)" + }, + "moi": { + "type": "number", + "description": "Moment of inertia of the pivot", + "default": 0, + "minimum": 0 + } + } +} diff --git a/src/main/resources/schemas/yams-shooter.schema.json b/src/main/resources/schemas/yams-shooter.schema.json new file mode 100644 index 00000000..53e5353c --- /dev/null +++ b/src/main/resources/schemas/yams-shooter.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "YAMS Shooter Configuration", + "description": "Configuration for a YAMS flywheel shooter mechanism (matches YamsShooterConfigurationJson.java)", + "type": "object", + "required": [ + "motorSetup", + "motorSystemId" + ], + "properties": { + "motorSetup": { + "$ref": "./motor-setup.schema.json", + "description": "Motor controller and hardware configuration" + }, + "motorSystemId": { + "$ref": "./motor-system-id.schema.json", + "description": "PID and feedforward control parameters" + }, + "lowerSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Lower software limit velocity (RPM or deg/s)" + }, + "upperSoftLimit": { + "$ref": "./unit-value.schema.json", + "description": "Upper software limit velocity (RPM or deg/s)" + }, + "gearing": { + "type": "array", + "description": "Gear reduction stages (e.g., [3, 4] means 3:4 reduction)", + "items": { + "type": "number", + "minimum": 0 + } + }, + "voltageCompensation": { + "$ref": "./unit-value.schema.json", + "description": "Voltage compensation value for simulation" + }, + "mass": { + "$ref": "./unit-value.schema.json", + "description": "Mass of the flywheel" + }, + "diameter": { + "$ref": "./unit-value.schema.json", + "description": "Diameter of the flywheel" + }, + "moi": { + "type": "number", + "description": "Moment of inertia of the flywheel", + "default": 0, + "minimum": 0 + } + } +} diff --git a/vendordeps/AdvantageKit.json b/vendordeps/AdvantageKit.json index 2707c2b7..91b4e342 100644 --- a/vendordeps/AdvantageKit.json +++ b/vendordeps/AdvantageKit.json @@ -1,9 +1,9 @@ { "fileName": "AdvantageKit.json", "name": "AdvantageKit", - "version": "4.1.2", + "version": "26.0.1", "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003", - "frcYear": "2025", + "frcYear": "2026", "mavenUrls": [ "https://frcmaven.wpi.edu/artifactory/littletonrobotics-mvn-release/" ], @@ -12,14 +12,14 @@ { "groupId": "org.littletonrobotics.akit", "artifactId": "akit-java", - "version": "4.1.2" + "version": "26.0.1" } ], "jniDependencies": [ { "groupId": "org.littletonrobotics.akit", "artifactId": "akit-wpilibio", - "version": "4.1.2", + "version": "26.0.1", "skipInvalidPlatforms": false, "isJar": false, "validPlatforms": [ diff --git a/vendordeps/PathplannerLib.json b/vendordeps/PathplannerLib-2026.1.2.json similarity index 84% rename from vendordeps/PathplannerLib.json rename to vendordeps/PathplannerLib-2026.1.2.json index 20b1b158..5f04ffa7 100644 --- a/vendordeps/PathplannerLib.json +++ b/vendordeps/PathplannerLib-2026.1.2.json @@ -1,9 +1,9 @@ { - "fileName": "PathplannerLib.json", + "fileName": "PathplannerLib-2026.1.2.json", "name": "PathplannerLib", - "version": "2025.2.7", + "version": "2026.1.2", "uuid": "1b42324f-17c6-4875-8e77-1c312bc8c786", - "frcYear": "2025", + "frcYear": "2026", "mavenUrls": [ "https://3015rangerrobotics.github.io/pathplannerlib/repo" ], @@ -12,7 +12,7 @@ { "groupId": "com.pathplanner.lib", "artifactId": "PathplannerLib-java", - "version": "2025.2.7" + "version": "2026.1.2" } ], "jniDependencies": [], @@ -20,7 +20,7 @@ { "groupId": "com.pathplanner.lib", "artifactId": "PathplannerLib-cpp", - "version": "2025.2.7", + "version": "2026.1.2", "libName": "PathplannerLib", "headerClassifier": "headers", "sharedLibrary": false, diff --git a/vendordeps/Phoenix5-frc2025-latest.json b/vendordeps/Phoenix5-replay-5.36.0.json similarity index 63% rename from vendordeps/Phoenix5-frc2025-latest.json rename to vendordeps/Phoenix5-replay-5.36.0.json index aa08d95b..7fbfcf5e 100644 --- a/vendordeps/Phoenix5-frc2025-latest.json +++ b/vendordeps/Phoenix5-replay-5.36.0.json @@ -1,64 +1,75 @@ { - "fileName": "Phoenix5-frc2025-latest.json", + "fileName": "Phoenix5-replay-5.36.0.json", "name": "CTRE-Phoenix (v5)", - "version": "5.35.1", - "frcYear": "2025", - "uuid": "ab676553-b602-441f-a38d-f1296eff6537", + "version": "5.36.0", + "frcYear": "2026", + "uuid": "fbc886a4-2cec-40c0-9835-71086a8cc3df", "mavenUrls": [ "https://maven.ctr-electronics.com/release/" ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-frc2025-latest.json", + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix/Phoenix5-replay-frc2026-latest.json", "requires": [ { - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", "errorMessage": "Phoenix 5 requires low-level libraries from Phoenix 6. Please add the Phoenix 6 vendordep before adding Phoenix 5.", - "offlineFileName": "Phoenix6-frc2025-latest.json", - "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2025-latest.json" + "offlineFileName": "Phoenix6-replay-frc2026-latest.json", + "onlineUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-replay-frc2026-latest.json" } ], "conflictsWith": [ { - "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", - "errorMessage": "Users must use the Phoenix 5 replay vendordep when using the Phoenix 6 replay vendordep.", - "offlineFileName": "Phoenix6-replay-frc2025-latest.json" + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "errorMessage": "Users must use the regular Phoenix 5 vendordep when using the regular Phoenix 6 vendordep.", + "offlineFileName": "Phoenix6-frc2026-latest.json" }, { - "uuid": "fbc886a4-2cec-40c0-9835-71086a8cc3df", + "uuid": "ab676553-b602-441f-a38d-f1296eff6537", "errorMessage": "Users cannot have both the replay and regular Phoenix 5 vendordeps in their robot program.", - "offlineFileName": "Phoenix5-replay-frc2025-latest.json" + "offlineFileName": "Phoenix5-frc2026-latest.json" } ], "javaDependencies": [ { "groupId": "com.ctre.phoenix", "artifactId": "api-java", - "version": "5.35.1" + "version": "5.36.0" }, { "groupId": "com.ctre.phoenix", "artifactId": "wpiapi-java", - "version": "5.35.1" + "version": "5.36.0" } ], "jniDependencies": [ { "groupId": "com.ctre.phoenix", "artifactId": "cci", - "version": "5.35.1", + "version": "5.36.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "cci-replay", + "version": "5.36.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" ], "simMode": "hwsim" }, { "groupId": "com.ctre.phoenix.sim", "artifactId": "cci-sim", - "version": "5.35.1", + "version": "5.36.0", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -74,15 +85,12 @@ { "groupId": "com.ctre.phoenix", "artifactId": "wpiapi-cpp", - "version": "5.35.1", + "version": "5.36.0", "libName": "CTRE_Phoenix_WPI", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -90,15 +98,12 @@ { "groupId": "com.ctre.phoenix", "artifactId": "api-cpp", - "version": "5.35.1", + "version": "5.36.0", "libName": "CTRE_Phoenix", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -106,23 +111,68 @@ { "groupId": "com.ctre.phoenix", "artifactId": "cci", - "version": "5.35.1", + "version": "5.36.0", "libName": "CTRE_PhoenixCCI", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "wpiapi-cpp-replay", + "version": "5.36.0", + "libName": "CTRE_Phoenix_WPIReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, "binaryPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "api-cpp-replay", + "version": "5.36.0", + "libName": "CTRE_PhoenixReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix.replay", + "artifactId": "cci-replay", + "version": "5.36.0", + "libName": "CTRE_PhoenixCCIReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxarm64", + "osxuniversal" ], "simMode": "hwsim" }, { "groupId": "com.ctre.phoenix.sim", "artifactId": "wpiapi-cpp-sim", - "version": "5.35.1", + "version": "5.36.0", "libName": "CTRE_Phoenix_WPISim", "headerClassifier": "headers", "sharedLibrary": true, @@ -138,7 +188,7 @@ { "groupId": "com.ctre.phoenix.sim", "artifactId": "api-cpp-sim", - "version": "5.35.1", + "version": "5.36.0", "libName": "CTRE_PhoenixSim", "headerClassifier": "headers", "sharedLibrary": true, @@ -154,7 +204,7 @@ { "groupId": "com.ctre.phoenix.sim", "artifactId": "cci-sim", - "version": "5.35.1", + "version": "5.36.0", "libName": "CTRE_PhoenixCCISim", "headerClassifier": "headers", "sharedLibrary": true, diff --git a/vendordeps/Phoenix6-frc2025-latest.json b/vendordeps/Phoenix6-replay-frc2026-latest.json similarity index 83% rename from vendordeps/Phoenix6-frc2025-latest.json rename to vendordeps/Phoenix6-replay-frc2026-latest.json index ce44ce4f..75532e9d 100644 --- a/vendordeps/Phoenix6-frc2025-latest.json +++ b/vendordeps/Phoenix6-replay-frc2026-latest.json @@ -1,38 +1,35 @@ { - "fileName": "Phoenix6-frc2025-latest.json", - "name": "CTRE-Phoenix (v6)", - "version": "25.4.0", - "frcYear": "2025", - "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "fileName": "Phoenix6-replay-frc2026-latest.json", + "name": "CTRE-Phoenix (v6) Replay", + "version": "26.1.1", + "frcYear": "2026", + "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", "mavenUrls": [ "https://maven.ctr-electronics.com/release/" ], - "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2025-latest.json", + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-replay-frc2026-latest.json", "conflictsWith": [ { - "uuid": "e7900d8d-826f-4dca-a1ff-182f658e98af", + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", "errorMessage": "Users can not have both the replay and regular Phoenix 6 vendordeps in their robot program.", - "offlineFileName": "Phoenix6-replay-frc2025-latest.json" + "offlineFileName": "Phoenix6-frc2026-latest.json" } ], "javaDependencies": [ { "groupId": "com.ctre.phoenix6", "artifactId": "wpiapi-java", - "version": "25.4.0" + "version": "26.1.1" } ], "jniDependencies": [ { "groupId": "com.ctre.phoenix6", "artifactId": "api-cpp", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -40,21 +37,32 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "tools", - "version": "25.4.0", + "version": "26.1.1", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "api-cpp-replay", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" ], "simMode": "hwsim" }, { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "api-cpp-sim", - "version": "25.4.0", + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "tools-replay", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -63,12 +71,12 @@ "linuxarm64", "osxuniversal" ], - "simMode": "swsim" + "simMode": "hwsim" }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "tools-sim", - "version": "25.4.0", + "artifactId": "api-cpp-sim", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -81,8 +89,8 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonSRX", - "version": "25.4.0", + "artifactId": "tools-sim", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -95,8 +103,8 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simVictorSPX", - "version": "25.4.0", + "artifactId": "simTalonSRX", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -109,8 +117,8 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simPigeonIMU", - "version": "25.4.0", + "artifactId": "simVictorSPX", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -123,8 +131,8 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simCANCoder", - "version": "25.4.0", + "artifactId": "simPigeonIMU", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -138,7 +146,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFX", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -152,7 +160,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFXS", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -166,7 +174,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANcoder", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -180,7 +188,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProPigeon2", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -194,7 +202,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANrange", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -208,7 +216,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdi", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -222,7 +230,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdle", - "version": "25.4.0", + "version": "26.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -238,15 +246,12 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "wpiapi-cpp", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_Phoenix6_WPI", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ - "windowsx86-64", - "linuxx86-64", - "linuxarm64", "linuxathena" ], "simMode": "hwsim" @@ -254,24 +259,37 @@ { "groupId": "com.ctre.phoenix6", "artifactId": "tools", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_PhoenixTools", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "wpiapi-cpp-replay", + "version": "26.1.1", + "libName": "CTRE_Phoenix6_WPIReplay", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, "binaryPlatforms": [ "windowsx86-64", "linuxx86-64", "linuxarm64", - "linuxathena" + "osxuniversal" ], "simMode": "hwsim" }, { - "groupId": "com.ctre.phoenix6.sim", - "artifactId": "wpiapi-cpp-sim", - "version": "25.4.0", - "libName": "CTRE_Phoenix6_WPISim", + "groupId": "com.ctre.phoenix6.replay", + "artifactId": "tools-replay", + "version": "26.1.1", + "libName": "CTRE_PhoenixTools_Replay", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -281,13 +299,13 @@ "linuxarm64", "osxuniversal" ], - "simMode": "swsim" + "simMode": "hwsim" }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "tools-sim", - "version": "25.4.0", - "libName": "CTRE_PhoenixTools_Sim", + "artifactId": "wpiapi-cpp-sim", + "version": "26.1.1", + "libName": "CTRE_Phoenix6_WPISim", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -301,9 +319,9 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simTalonSRX", - "version": "25.4.0", - "libName": "CTRE_SimTalonSRX", + "artifactId": "tools-sim", + "version": "26.1.1", + "libName": "CTRE_PhoenixTools_Sim", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -317,9 +335,9 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simVictorSPX", - "version": "25.4.0", - "libName": "CTRE_SimVictorSPX", + "artifactId": "simTalonSRX", + "version": "26.1.1", + "libName": "CTRE_SimTalonSRX", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -333,9 +351,9 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simPigeonIMU", - "version": "25.4.0", - "libName": "CTRE_SimPigeonIMU", + "artifactId": "simVictorSPX", + "version": "26.1.1", + "libName": "CTRE_SimVictorSPX", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -349,9 +367,9 @@ }, { "groupId": "com.ctre.phoenix6.sim", - "artifactId": "simCANCoder", - "version": "25.4.0", - "libName": "CTRE_SimCANCoder", + "artifactId": "simPigeonIMU", + "version": "26.1.1", + "libName": "CTRE_SimPigeonIMU", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -366,7 +384,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFX", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProTalonFX", "headerClassifier": "headers", "sharedLibrary": true, @@ -382,7 +400,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProTalonFXS", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProTalonFXS", "headerClassifier": "headers", "sharedLibrary": true, @@ -398,7 +416,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANcoder", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProCANcoder", "headerClassifier": "headers", "sharedLibrary": true, @@ -414,7 +432,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProPigeon2", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProPigeon2", "headerClassifier": "headers", "sharedLibrary": true, @@ -430,7 +448,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANrange", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProCANrange", "headerClassifier": "headers", "sharedLibrary": true, @@ -446,7 +464,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdi", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProCANdi", "headerClassifier": "headers", "sharedLibrary": true, @@ -462,7 +480,7 @@ { "groupId": "com.ctre.phoenix6.sim", "artifactId": "simProCANdle", - "version": "25.4.0", + "version": "26.1.1", "libName": "CTRE_SimProCANdle", "headerClassifier": "headers", "sharedLibrary": true, diff --git a/vendordeps/REVLib.json b/vendordeps/REVLib.json index 459a62f7..1d80ce77 100644 --- a/vendordeps/REVLib.json +++ b/vendordeps/REVLib.json @@ -1,25 +1,55 @@ { "fileName": "REVLib.json", "name": "REVLib", - "version": "2025.0.3", - "frcYear": "2025", + "version": "2026.0.3", + "frcYear": "2026", "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", "mavenUrls": [ "https://maven.revrobotics.com/" ], - "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2025.json", + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2026.json", "javaDependencies": [ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-java", - "version": "2025.0.3" + "version": "2026.0.3" } ], "jniDependencies": [ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-driver", - "version": "2025.0.3", + "version": "2026.0.3", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "RevLibBackendDriver", + "version": "2026.0.3", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "RevLibWpiBackendDriver", + "version": "2026.0.3", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -36,7 +66,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-cpp", - "version": "2025.0.3", + "version": "2026.0.3", "libName": "REVLib", "headerClassifier": "headers", "sharedLibrary": false, @@ -53,7 +83,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-driver", - "version": "2025.0.3", + "version": "2026.0.3", "libName": "REVLibDriver", "headerClassifier": "headers", "sharedLibrary": false, @@ -66,6 +96,38 @@ "linuxarm32", "osxuniversal" ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "RevLibBackendDriver", + "version": "2026.0.3", + "libName": "BackendDriver", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "RevLibWpiBackendDriver", + "version": "2026.0.3", + "libName": "REVLibWpi", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] } ] } diff --git a/vendordeps/ReduxLib-2025.0.1.json b/vendordeps/ReduxLib-2026.1.1.json similarity index 74% rename from vendordeps/ReduxLib-2025.0.1.json rename to vendordeps/ReduxLib-2026.1.1.json index 3a66d722..9081f8b2 100644 --- a/vendordeps/ReduxLib-2025.0.1.json +++ b/vendordeps/ReduxLib-2026.1.1.json @@ -1,31 +1,30 @@ { - "fileName": "ReduxLib-2025.0.1.json", + "fileName": "ReduxLib-2026.1.1.json", "name": "ReduxLib", - "version": "2025.0.1", - "frcYear": "2025", + "version": "2026.1.1", + "frcYear": "2026", "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", "mavenUrls": [ "https://maven.reduxrobotics.com/" ], - "jsonUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2025.json", + "jsonUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2026.json", "javaDependencies": [ { "groupId": "com.reduxrobotics.frc", "artifactId": "ReduxLib-java", - "version": "2025.0.1" + "version": "2026.1.1" } ], "jniDependencies": [ { "groupId": "com.reduxrobotics.frc", - "artifactId": "ReduxLib-driver", - "version": "2025.0.1", + "artifactId": "ReduxLib-fifo", + "version": "2026.1.1", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ "linuxathena", "linuxx86-64", - "linuxarm32", "linuxarm64", "osxuniversal", "windowsx86-64" @@ -36,7 +35,7 @@ { "groupId": "com.reduxrobotics.frc", "artifactId": "ReduxLib-cpp", - "version": "2025.0.1", + "version": "2026.1.1", "libName": "ReduxLib", "headerClassifier": "headers", "sourcesClassifier": "sources", @@ -45,7 +44,6 @@ "binaryPlatforms": [ "linuxathena", "linuxx86-64", - "linuxarm32", "linuxarm64", "osxuniversal", "windowsx86-64" @@ -53,16 +51,15 @@ }, { "groupId": "com.reduxrobotics.frc", - "artifactId": "ReduxLib-driver", - "version": "2025.0.1", - "libName": "ReduxCore", + "artifactId": "ReduxLib-fifo", + "version": "2026.1.1", + "libName": "reduxfifo", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ "linuxathena", "linuxx86-64", - "linuxarm32", "linuxarm64", "osxuniversal", "windowsx86-64" diff --git a/vendordeps/Studica-2025.0.1.json b/vendordeps/Studica.json similarity index 70% rename from vendordeps/Studica-2025.0.1.json rename to vendordeps/Studica.json index a3ed1fee..daf14345 100644 --- a/vendordeps/Studica-2025.0.1.json +++ b/vendordeps/Studica.json @@ -1,71 +1,71 @@ { - "fileName": "Studica-2025.0.1.json", + "fileName": "Studica.json", "name": "Studica", - "version": "2025.0.1", + "version": "2026.0.0", + "frcYear": "2026", "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", - "frcYear": "2025", "mavenUrls": [ - "https://dev.studica.com/maven/release/2025/" + "https://dev.studica.com/maven/release/2026/" ], - "jsonUrl": "https://dev.studica.com/releases/2025/Studica-2025.0.1.json", - "cppDependencies": [ + "jsonUrl": "https://dev.studica.com/maven/release/2026/json/Studica-2026.0.0.json", + "javaDependencies": [ { - "artifactId": "Studica-cpp", - "binaryPlatforms": [ - "linuxathena", - "linuxarm32", - "linuxarm64", - "linuxx86-64", - "osxuniversal", - "windowsx86-64" - ], "groupId": "com.studica.frc", - "headerClassifier": "headers", - "libName": "Studica", - "sharedLibrary": false, - "skipInvalidPlatforms": true, - "version": "2025.0.1" - }, + "artifactId": "Studica-java", + "version": "2026.0.0" + } + ], + "jniDependencies": [ { + "groupId": "com.studica.frc", "artifactId": "Studica-driver", - "binaryPlatforms": [ - "linuxathena", - "linuxarm32", + "version": "2026.0.0", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", "linuxarm64", "linuxx86-64", - "osxuniversal", - "windowsx86-64" - ], + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { "groupId": "com.studica.frc", + "artifactId": "Studica-cpp", + "version": "2026.0.0", + "libName": "Studica", "headerClassifier": "headers", - "libName": "StudicaDriver", "sharedLibrary": false, "skipInvalidPlatforms": true, - "version": "2025.0.1" - } - ], - "javaDependencies": [ + "binaryPlatforms": [ + "windowsx86-64", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, { - "artifactId": "Studica-java", "groupId": "com.studica.frc", - "version": "2025.0.1" - } - ], - "jniDependencies": [ - { "artifactId": "Studica-driver", - "groupId": "com.studica.frc", - "isJar": false, + "version": "2026.0.0", + "libName": "StudicaDriver", + "headerClassifier": "headers", + "sharedLibrary": false, "skipInvalidPlatforms": true, - "validPlatforms": [ - "linuxathena", - "linuxarm32", + "binaryPlatforms": [ + "windowsx86-64", "linuxarm64", "linuxx86-64", - "osxuniversal", - "windowsx86-64" - ], - "version": "2025.0.1" + "linuxathena", + "linuxarm32", + "osxuniversal" + ] } ] } diff --git a/vendordeps/ThriftyLib.json b/vendordeps/ThriftyLib-2026.1.0.json similarity index 62% rename from vendordeps/ThriftyLib.json rename to vendordeps/ThriftyLib-2026.1.0.json index 5c3595e3..ec3338c5 100644 --- a/vendordeps/ThriftyLib.json +++ b/vendordeps/ThriftyLib-2026.1.0.json @@ -1,18 +1,18 @@ { - "fileName": "ThriftyLib.json", + "fileName": "ThriftyLib-2026.1.0.json", "name": "ThriftyLib", - "version": "2025.1.1", - "frcYear": "2025", + "version": "2026.1.0", + "frcYear": "2026", "uuid": "60b2694b-9e6e-4026-81ee-6f167946f4b0", "mavenUrls": [ "https://docs.home.thethriftybot.com" ], - "jsonUrl": "https://docs.home.thethriftybot.com/ThriftyLib.json", + "jsonUrl": "https://docs.home.thethriftybot.com/ThriftyLib-2026.json", "javaDependencies": [ { "groupId": "com.thethriftybot.frc", "artifactId": "ThriftyLib-java", - "version": "2025.1.1" + "version": "2026.1.0" } ], "jniDependencies": [], diff --git a/vendordeps/WPILibNewCommands.json b/vendordeps/WPILibNewCommands.json index 3718e0ac..d90630e9 100644 --- a/vendordeps/WPILibNewCommands.json +++ b/vendordeps/WPILibNewCommands.json @@ -3,7 +3,7 @@ "name": "WPILib-New-Commands", "version": "1.0.0", "uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266", - "frcYear": "2025", + "frcYear": "2026", "mavenUrls": [], "jsonUrl": "", "javaDependencies": [ @@ -25,6 +25,7 @@ "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ + "linuxsystemcore", "linuxathena", "linuxarm32", "linuxarm64", diff --git a/vendordeps/libgrapplefrc2025.json b/vendordeps/libgrapplefrc2026.json similarity index 83% rename from vendordeps/libgrapplefrc2025.json rename to vendordeps/libgrapplefrc2026.json index 4cb72532..d804c19f 100644 --- a/vendordeps/libgrapplefrc2025.json +++ b/vendordeps/libgrapplefrc2026.json @@ -1,30 +1,29 @@ { - "fileName": "libgrapplefrc2025.json", + "fileName": "libgrapplefrc2026.json", "name": "libgrapplefrc", - "version": "2025.1.3", - "frcYear": "2025", + "version": "2026.0.0", + "frcYear": "2026", "uuid": "8ef3423d-9532-4665-8339-206dae1d7168", "mavenUrls": [ "https://storage.googleapis.com/grapple-frc-maven" ], - "jsonUrl": "https://storage.googleapis.com/grapple-frc-maven/libgrapplefrc2025.json", + "jsonUrl": "https://storage.googleapis.com/grapple-frc-maven/libgrapplefrc2026.json", "javaDependencies": [ { "groupId": "au.grapplerobotics", "artifactId": "libgrapplefrcjava", - "version": "2025.1.3" + "version": "2026.0.0" } ], "jniDependencies": [ { "groupId": "au.grapplerobotics", "artifactId": "libgrapplefrcdriver", - "version": "2025.1.3", + "version": "2026.0.0", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ "windowsx86-64", - "windowsx86", "linuxarm64", "linuxx86-64", "linuxathena", @@ -37,14 +36,13 @@ { "groupId": "au.grapplerobotics", "artifactId": "libgrapplefrccpp", - "version": "2025.1.3", + "version": "2026.0.0", "libName": "grapplefrc", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ "windowsx86-64", - "windowsx86", "linuxarm64", "linuxx86-64", "linuxathena", @@ -55,14 +53,13 @@ { "groupId": "au.grapplerobotics", "artifactId": "libgrapplefrcdriver", - "version": "2025.1.3", + "version": "2026.0.0", "libName": "grapplefrcdriver", "headerClassifier": "headers", "sharedLibrary": true, "skipInvalidPlatforms": true, "binaryPlatforms": [ "windowsx86-64", - "windowsx86", "linuxarm64", "linuxx86-64", "linuxathena", diff --git a/vendordeps/maple-sim.json b/vendordeps/maple-sim.json deleted file mode 100644 index 65f44133..00000000 --- a/vendordeps/maple-sim.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "fileName": "maple-sim.json", - "name": "maplesim", - "version": "0.3.14", - "frcYear": "2025", - "uuid": "c39481e8-4a63-4a4c-9df6-48d91e4da37b", - "mavenUrls": [ - "https://shenzhen-robotics-alliance.github.io/maple-sim/vendordep/repos/releases", - "https://repo1.maven.org/maven2" - ], - "jsonUrl": "https://shenzhen-robotics-alliance.github.io/maple-sim/vendordep/maple-sim.json", - "javaDependencies": [ - { - "groupId": "org.ironmaple", - "artifactId": "maplesim-java", - "version": "0.3.14" - }, - { - "groupId": "org.dyn4j", - "artifactId": "dyn4j", - "version": "5.0.2" - } - ], - "jniDependencies": [], - "cppDependencies": [] -} diff --git a/vendordeps/photonlib.json b/vendordeps/photonlib.json index 2b5b6ad5..62661dd2 100644 --- a/vendordeps/photonlib.json +++ b/vendordeps/photonlib.json @@ -1,9 +1,9 @@ { "fileName": "photonlib.json", "name": "photonlib", - "version": "v2025.3.2", + "version": "v2026.3.1", "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004", - "frcYear": "2025", + "frcYear": "2026", "mavenUrls": [ "https://maven.photonvision.org/repository/internal", "https://maven.photonvision.org/repository/snapshots" @@ -13,7 +13,7 @@ { "groupId": "org.photonvision", "artifactId": "photontargeting-cpp", - "version": "v2025.3.2", + "version": "v2026.3.1", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -28,7 +28,7 @@ { "groupId": "org.photonvision", "artifactId": "photonlib-cpp", - "version": "v2025.3.2", + "version": "v2026.3.1", "libName": "photonlib", "headerClassifier": "headers", "sharedLibrary": true, @@ -43,7 +43,7 @@ { "groupId": "org.photonvision", "artifactId": "photontargeting-cpp", - "version": "v2025.3.2", + "version": "v2026.3.1", "libName": "photontargeting", "headerClassifier": "headers", "sharedLibrary": true, @@ -60,12 +60,12 @@ { "groupId": "org.photonvision", "artifactId": "photonlib-java", - "version": "v2025.3.2" + "version": "v2026.3.1" }, { "groupId": "org.photonvision", "artifactId": "photontargeting-java", - "version": "v2025.3.2" + "version": "v2026.3.1" } ] } diff --git a/vendordeps/questnavlib.json b/vendordeps/questnavlib.json new file mode 100644 index 00000000..4966f820 --- /dev/null +++ b/vendordeps/questnavlib.json @@ -0,0 +1,21 @@ +{ + "fileName": "questnavlib.json", + "name": "questnavlib", + "version": "2026-2.1.0", + "uuid": "a706fe68-86e5-4aed-92c5-ce05aca007f0", + "frcYear": "2026", + "mavenUrls": [ + "https://maven.questnav.gg/releases", + "https://maven.questnav.gg/snapshots" + ], + "jsonUrl": "https://maven.questnav.gg/snapshots/gg/questnav/questnavlib-json/2026-2.1.0/questnavlib-json-2026-2.1.0.json", + "javaDependencies": [ + { + "groupId": "gg.questnav", + "artifactId": "questnavlib-java", + "version": "2026-2.1.0" + } + ], + "cppDependencies": [], + "jniDependencies": [] +} diff --git a/vendordeps/yagsl-2026.2.27.1.json b/vendordeps/yagsl-2026.2.27.1.json new file mode 100644 index 00000000..0f99fac5 --- /dev/null +++ b/vendordeps/yagsl-2026.2.27.1.json @@ -0,0 +1,46 @@ +{ + "fileName": "yagsl-2026.2.27.1.json", + "name": "YAGSL", + "version": "2026.2.27.1", + "frcYear": "2026", + "uuid": "1ccce5a4-acd2-4d18-bca3-4b8047188400", + "mavenUrls": [ + "https://yet-another-software-suite.github.io/YAGSL/releases/", + "https://repo1.maven.org/maven2" + ], + "jsonUrl": "https://yet-another-software-suite.github.io/YAGSL/yagsl.json", + "javaDependencies": [ + { + "groupId": "swervelib", + "artifactId": "YAGSL-java", + "version": "2026.2.27.1" + }, + { + "groupId": "org.dyn4j", + "artifactId": "dyn4j", + "version": "5.0.2" + } + ], + "requires": [ + { + "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", + "errorMessage": "ReduxLib is required!", + "offlineFileName": "ReduxLib.json", + "onlineUrl": "https://frcsdk.reduxrobotics.com/ReduxLib_2026.json" + }, + { + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "errorMessage": "REVLib is required!", + "offlineFileName": "REVLib.json", + "onlineUrl": "https://software-metadata.revrobotics.com/REVLib-2026.json" + }, + { + "uuid": "60b2694b-9e6e-4026-81ee-6f167946f4b0", + "errorMessage": "ThriftyLib is required!", + "offlineFileName": "ThriftyLib.json", + "onlineUrl": "https://docs.home.thethriftybot.com/ThriftyLib-2026.json" + } + ], + "jniDependencies": [], + "cppDependencies": [] +} diff --git a/vendordeps/yams.json b/vendordeps/yams.json index 1ce21095..597f32f1 100644 --- a/vendordeps/yams.json +++ b/vendordeps/yams.json @@ -1,8 +1,8 @@ { "fileName": "yams.json", "name": "Yet Another Mechanism System", - "version": "2025.11.20", - "frcYear": "2025", + "version": "2026.2.24", + "frcYear": "2026", "uuid": "a1051e86-a979-4880-a28b-a0d5362d1d96", "mavenUrls": [ "https://yet-another-software-suite.github.io/YAMS/releases/" @@ -12,9 +12,10 @@ { "groupId": "yams", "artifactId": "YAMS-java", - "version": "2025.11.20" + "version": "2026.2.24" } ], "cppDependencies": [], - "jniDependencies": [] + "jniDependencies": [], + "requires": [] } From 553e303174ab84cb931a5b60dec5907a3890a3b1 Mon Sep 17 00:00:00 2001 From: 5010 FHS 6 Date: Wed, 1 Apr 2026 17:37:02 -0400 Subject: [PATCH 51/93] Tweak shooter behavior Bump build metadata (GIT_REVISION/sha/date and BUILD_DATE/BUILD_UNIX_TIME). Adjust shooter control: change SPEED2/3/4 values (SPEED2 increased to 0.35, SPEED3/4 shifted), switch from velocity-based setVelocity(RPM) calls to direct set(speed) with an UPPEROFFSET applied to the upper flywheel. Update LED logic for the shooter to use setSegmentActive and a rainbow pattern while active, and restore segment state in finallyDo. Add onFalse handlers for LEFT_BUMPER and RIGHT_BUMPER to stop feeder and flywheel motors. Minor comment/whitespace cleanup around a StateMachine remark. --- .../java/frc/robot/blackteam/BlackRobot.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 18f39cb9..a446a827 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -23,9 +23,9 @@ public class BlackRobot extends GenericRobot { private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); private final double SPEED1 = 0.15; - private final double SPEED2 = 0.16; - private final double SPEED3 = 0.17; - private final double SPEED4 = 0.18; + private final double SPEED2 = 0.35; + private final double SPEED3 = 0.16; + private final double SPEED4 = 0.17; private final double UPPEROFFSET = 0.01; private final AngularVelocity VELOCITY2 = RPM.of(1500); private final String FEEDER_LED = "left_half"; @@ -72,16 +72,15 @@ public void configureButtonBindings(Controller driver, Controller operator) { .createXButton() .whileTrue( lowerFlyWheel - .setVelocity(VELOCITY2) - .alongWith(upperFlyWheel.setVelocity(VELOCITY2)) + .set(SPEED2) + .alongWith(upperFlyWheel.set(SPEED2 + UPPEROFFSET)) .beforeStarting( - () -> - LEDStrip.changeSegmentPattern( - SHOOTER_LED, LEDStrip.getSolidPattern(Color.kAquamarine))) - .finallyDo( - () -> - LEDStrip.changeSegmentPattern( - SHOOTER_LED, LEDStrip.getSolidPattern(Color.kRed)))); + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, true); + LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); + }) + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + driver .createYButton() .whileTrue( @@ -126,6 +125,9 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); + driver.LEFT_BUMPER.onFalse( + (feeder.setSpeed(0)).andThen(lowerFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + driver .createRightBumper() .whileTrue( @@ -143,6 +145,9 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); + driver.RIGHT_BUMPER.onFalse( + (feeder.setSpeed(0)).andThen(upperFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + State prep = flyWheelStateMachine.addState( "prep", @@ -156,12 +161,12 @@ public void configureButtonBindings(Controller driver, Controller operator) { flyWheelStateMachine.setInitialState(prep); prep.switchTo(fire).when(lowerFlyWheel.isNearTarget(RPM.of(200))); - // rightBumper.whileTrue(flyWheelStateMachine); + // rightBumper.whileTrue(flyWheelStateMachine); // rightBumper.onFalse( - // lowerFlyWheel - // .setVelocity(RPM.of(0)) - // .andThen(lowerFlyWheel.set(0)) - // .alongWith(feeder.setSpeed(0))); + // lowerFlyWheel + // .setVelocity(RPM.of(0)) + // .andThen(lowerFlyWheel.set(0)) + // .alongWith(feeder.setSpeed(0))); } @Override From 5cbc3723eb7cf106d65679535dc22ebe0b14e392 Mon Sep 17 00:00:00 2001 From: 5010 FHS 6 Date: Wed, 1 Apr 2026 19:03:40 -0400 Subject: [PATCH 52/93] Normalize flywheel configs and add LobbinLoco Convert devices in upper/lower flywheel subsystems to an array format and update their flywheel configs: rename simMotorSystemId -> simSystemId, stages -> gearStages, and diameter -> radius (diameter 4in represented as radius 2in). Update ReflectionsManager mapping for THRIFTYBOT to com.thethriftybot.devices.ThriftyNova. Add a break for the 'nova'/'thriftynova'/'thrifty_nova' cases in YamsConfigCommon to avoid applying the default open-loop ramp behavior. Add a new LobbinLoco.json with field dimensions and tag poses. --- .../black_robot/subsystems/lowerflywheel.json | 9 +- .../subsystems/lowerflywheel/flywheel.json | 8 +- .../black_robot/subsystems/upperflywheel.json | 9 +- .../subsystems/upperflywheel/flywheel.json | 8 +- .../json/devices/ReflectionsManager.java | 2 +- .../config/json/devices/YamsConfigCommon.java | 1 + .../org/frc5010/lobbinloco/LobbinLoco.json | 116 ++++++++++++++++++ 7 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/frc5010/lobbinloco/LobbinLoco.json diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel.json index 7ce8b5e6..229cec66 100644 --- a/src/main/deploy/black_robot/subsystems/lowerflywheel.json +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel.json @@ -1,5 +1,8 @@ { - "devices": { - "yams_shooter": "lowerflywheel/flywheel.json" - } + "devices": [ + { + "device": "yams_shooter", + "file": "lowerflywheel/flywheel.json" + } + ] } diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json index f13dbf5c..e1b0df38 100644 --- a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json @@ -17,7 +17,7 @@ "a": 0.098446 } }, - "simMotorSystemId": { + "simSystemId": { "feedBack": { "p": 0, "i": 0.0, @@ -37,13 +37,13 @@ "val": 10000, "uom": "rpm" }, - "stages": "52:24", + "gearStages": "52:24", "mass": { "val": 2, "uom": "kg" }, - "diameter": { - "val": 4, + "radius": { + "val": 2, "uom": "in" } } diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel.json index 9056d051..41742573 100644 --- a/src/main/deploy/black_robot/subsystems/upperflywheel.json +++ b/src/main/deploy/black_robot/subsystems/upperflywheel.json @@ -1,5 +1,8 @@ { - "devices": { - "yams_shooter": "upperflywheel/flywheel.json" - } + "devices": [ + { + "device": "yams_shooter", + "file": "upperflywheel/flywheel.json" + } + ] } diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json index c316713e..dd3663bd 100644 --- a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json @@ -17,7 +17,7 @@ "a": 0.098446 } }, - "simMotorSystemId": { + "simSystemId": { "feedBack": { "p": 0, "i": 0.0, @@ -37,13 +37,13 @@ "val": 10000, "uom": "rpm" }, - "stages": "52:24", + "gearStages": "52:24", "mass": { "val": 2, "uom": "kg" }, - "diameter": { - "val": 4, + "radius": { + "val": 2, "uom": "in" } } diff --git a/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java b/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java index 85eb899f..419546d2 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java +++ b/src/main/java/org/frc5010/common/config/json/devices/ReflectionsManager.java @@ -29,7 +29,7 @@ public enum SparkBaseType { put(VENDOR.REV, "com.revrobotics.spark.SparkBase"); put(VENDOR.PHOENIX5, "com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX"); put(VENDOR.PHOENIX6, "com.ctre.phoenix6.hardware.TalonFXS"); - put(VENDOR.THRIFTYBOT, "com.thethriftybot.ThriftyNova"); + put(VENDOR.THRIFTYBOT, "com.thethriftybot.devices.ThriftyNova"); put(VENDOR.YAMS, "yams.motorcontrollers.SmartMotorController"); } }; diff --git a/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java b/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java index 314728b8..ddbad3da 100644 --- a/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java +++ b/src/main/java/org/frc5010/common/config/json/devices/YamsConfigCommon.java @@ -149,6 +149,7 @@ public static Optional configureSmartMotorController( case "nova": case "thriftynova": case "thrifty_nova": + break; default: motorConfig.withOpenLoopRampRate(UnitsParser.parseTime(motorSystemId.openLoopRamp)); break; diff --git a/src/main/java/org/frc5010/lobbinloco/LobbinLoco.json b/src/main/java/org/frc5010/lobbinloco/LobbinLoco.json new file mode 100644 index 00000000..477386e3 --- /dev/null +++ b/src/main/java/org/frc5010/lobbinloco/LobbinLoco.json @@ -0,0 +1,116 @@ +{ + "field": { + "width": 6.095996708161778, + "length": 15.239991770404444 + }, + "tags": [ + { + "ID": 17, + "pose": { + "translation": { + "x": 8.229595556018399, + "y": 4.876797366529422, + "z": 1.0382244393588027 + }, + "rotation": { + "quaternion": { + "X": 0, + "Y": 0, + "Z": 1, + "W": 6.123233995736766e-17 + } + } + } + }, + { + "ID": 18, + "pose": { + "translation": { + "x": 8.229595556018399, + "y": 1.2191993416323554, + "z": 1.0382244393588027 + }, + "rotation": { + "quaternion": { + "X": 0, + "Y": 0, + "Z": 1, + "W": 6.123233995736766e-17 + } + } + } + }, + { + "ID": 19, + "pose": { + "translation": { + "x": 11.887193580915467, + "y": 2.489198655832726, + "z": 1.0382244393588027 + }, + "rotation": { + "quaternion": { + "X": 0, + "Y": 0, + "Z": 1, + "W": 6.123233995736766e-17 + } + } + } + }, + { + "ID": 20, + "pose": { + "translation": { + "x": 11.887193580915467, + "y": 3.505198107193022, + "z": 1.0382244393588027 + }, + "rotation": { + "quaternion": { + "X": 0, + "Y": 0, + "Z": 1, + "W": 6.123233995736766e-17 + } + } + } + }, + { + "ID": 21, + "pose": { + "translation": { + "x": 14.325592264180177, + "y": 5.28319714707354, + "z": 1.0382244393588027 + }, + "rotation": { + "quaternion": { + "X": 0, + "Y": 0, + "Z": 1, + "W": 6.123233995736766e-17 + } + } + } + }, + { + "ID": 22, + "pose": { + "translation": { + "x": 14.325592264180177, + "y": 0.7111996159522074, + "z": 1.0382244393588027 + }, + "rotation": { + "quaternion": { + "X": 0, + "Y": 0, + "Z": 1, + "W": 6.123233995736766e-17 + } + } + } + } + ] +} From 3be7007dbc3e33504b3df4e94867a4a4888cdbea Mon Sep 17 00:00:00 2001 From: Curt Date: Sat, 4 Apr 2026 11:18:09 -0400 Subject: [PATCH 53/93] Remove unused robots --- src/main/deploy/baby_swerve/cameras.json | 3 - .../baby_swerve/cameras/localization.json | 13 -- .../deploy/baby_swerve/competition_mode.json | 6 - src/main/deploy/baby_swerve/controllers.json | 5 - .../controllers/axis/driver_left_trigger.json | 8 - .../controllers/axis/driver_left_x.json | 8 - .../controllers/axis/driver_left_y.json | 8 - .../axis/driver_right_trigger.json | 8 - .../controllers/axis/driver_right_x.json | 8 - .../controllers/axis/operator_left_y.json | 9 - .../controllers/axis/operator_right_y.json | 9 - .../baby_swerve/controllers/driver.json | 11 -- .../baby_swerve/controllers/operator.json | 8 - src/main/deploy/baby_swerve/demo_mode.json | 6 - .../baby_swerve/drive_modules/backleft.json | 5 - .../baby_swerve/drive_modules/backright.json | 5 - .../baby_swerve/drive_modules/frontleft.json | 5 - .../baby_swerve/drive_modules/frontright.json | 5 - src/main/deploy/baby_swerve/robot.json | 13 -- .../deploy/baby_swerve/yagsl_drivetrain.json | 10 - .../yagsl_swerve/controllerproperties.json | 8 - .../yagsl_swerve/modules/backleft.json | 27 --- .../yagsl_swerve/modules/backright.json | 27 --- .../yagsl_swerve/modules/frontleft.json | 27 --- .../yagsl_swerve/modules/frontright.json | 27 --- .../modules/physicalproperties.json | 24 --- .../yagsl_swerve/modules/pidfproperties.json | 16 -- .../baby_swerve/yagsl_swerve/swervedrive.json | 14 -- .../basic_robot/akit_swerve_drivetrain.json | 178 ----------------- src/main/deploy/basic_robot/cameras.json | 9 - .../deploy/basic_robot/cameras/intake.json | 13 -- src/main/deploy/basic_robot/cameras/left.json | 12 -- .../basic_robot/cameras/localization.json | 12 -- .../deploy/basic_robot/cameras/quest.json | 11 -- .../deploy/basic_robot/cameras/right.json | 12 -- .../deploy/basic_robot/cameras/shooter.json | 20 -- .../deploy/basic_robot/competition_mode.json | 6 - src/main/deploy/basic_robot/controllers.json | 6 - .../controllers/axis/driver_left_trigger.json | 8 - .../controllers/axis/driver_left_x.json | 8 - .../controllers/axis/driver_left_y.json | 8 - .../axis/driver_right_trigger.json | 8 - .../controllers/axis/driver_right_x.json | 8 - .../controllers/axis/operator_left_y.json | 9 - .../controllers/axis/operator_right_y.json | 9 - .../basic_robot/controllers/driver.json | 11 -- .../basic_robot/controllers/operator.json | 8 - src/main/deploy/basic_robot/demo_mode.json | 6 - .../basic_robot/drive_modules/backleft.json | 5 - .../basic_robot/drive_modules/backright.json | 5 - .../basic_robot/drive_modules/frontleft.json | 5 - .../basic_robot/drive_modules/frontright.json | 5 - .../deploy/basic_robot/field/game_pieces.json | 10 - src/main/deploy/basic_robot/robot.json | 15 -- .../basic_robot/subsystems/example.json | 24 --- .../subsystems/example/percent_motor.json | 11 -- .../subsystems/example/velocity_motor.json | 18 -- .../subsystems/example/yams_arm.json | 102 ---------- .../subsystems/example/yams_elevator.json | 98 ---------- .../subsystems/example/yams_pivot.json | 96 ---------- .../subsystems/example/yams_shooter.json | 70 ------- .../basic_robot/subsystems/led_strip.json | 16 -- .../deploy/basic_robot/yagsl_drivetrain.json | 24 --- .../yagsl_swerve/controllerproperties.json | 8 - .../yagsl_swerve/modules/backleft.json | 26 --- .../yagsl_swerve/modules/backright.json | 26 --- .../yagsl_swerve/modules/frontleft.json | 26 --- .../yagsl_swerve/modules/frontright.json | 26 --- .../modules/physicalproperties.json | 24 --- .../yagsl_swerve/modules/pidfproperties.json | 16 -- .../basic_robot/yagsl_swerve/swervedrive.json | 14 -- .../frc/robot/baby_swerve/BabySwerve.java | 43 ----- .../robot/example/ConfiguredMechanisms.java | 80 -------- .../robot/example/DisplayValueSubsystem.java | 76 -------- .../java/frc/robot/example/ExampleRobot.java | 59 ------ .../example/commands/ExampleCommands.java | 131 ------------- .../robot/example/subsystems/ExampleIO.java | 79 -------- .../example/subsystems/ExampleIOReal.java | 117 ------------ .../example/subsystems/ExampleIOSim.java | 115 ----------- .../example/subsystems/ExampleSubsystem.java | 179 ------------------ 80 files changed, 2234 deletions(-) delete mode 100644 src/main/deploy/baby_swerve/cameras.json delete mode 100644 src/main/deploy/baby_swerve/cameras/localization.json delete mode 100644 src/main/deploy/baby_swerve/competition_mode.json delete mode 100644 src/main/deploy/baby_swerve/controllers.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/driver_left_trigger.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/driver_left_x.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/driver_left_y.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/driver_right_trigger.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/driver_right_x.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/operator_left_y.json delete mode 100644 src/main/deploy/baby_swerve/controllers/axis/operator_right_y.json delete mode 100644 src/main/deploy/baby_swerve/controllers/driver.json delete mode 100644 src/main/deploy/baby_swerve/controllers/operator.json delete mode 100644 src/main/deploy/baby_swerve/demo_mode.json delete mode 100644 src/main/deploy/baby_swerve/drive_modules/backleft.json delete mode 100644 src/main/deploy/baby_swerve/drive_modules/backright.json delete mode 100644 src/main/deploy/baby_swerve/drive_modules/frontleft.json delete mode 100644 src/main/deploy/baby_swerve/drive_modules/frontright.json delete mode 100644 src/main/deploy/baby_swerve/robot.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_drivetrain.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/controllerproperties.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/modules/backleft.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/modules/backright.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/modules/frontleft.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/modules/frontright.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/modules/physicalproperties.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json delete mode 100644 src/main/deploy/baby_swerve/yagsl_swerve/swervedrive.json delete mode 100644 src/main/deploy/basic_robot/akit_swerve_drivetrain.json delete mode 100644 src/main/deploy/basic_robot/cameras.json delete mode 100644 src/main/deploy/basic_robot/cameras/intake.json delete mode 100644 src/main/deploy/basic_robot/cameras/left.json delete mode 100644 src/main/deploy/basic_robot/cameras/localization.json delete mode 100644 src/main/deploy/basic_robot/cameras/quest.json delete mode 100644 src/main/deploy/basic_robot/cameras/right.json delete mode 100644 src/main/deploy/basic_robot/cameras/shooter.json delete mode 100644 src/main/deploy/basic_robot/competition_mode.json delete mode 100644 src/main/deploy/basic_robot/controllers.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/driver_left_trigger.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/driver_left_x.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/driver_left_y.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/driver_right_trigger.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/driver_right_x.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/operator_left_y.json delete mode 100644 src/main/deploy/basic_robot/controllers/axis/operator_right_y.json delete mode 100644 src/main/deploy/basic_robot/controllers/driver.json delete mode 100644 src/main/deploy/basic_robot/controllers/operator.json delete mode 100644 src/main/deploy/basic_robot/demo_mode.json delete mode 100644 src/main/deploy/basic_robot/drive_modules/backleft.json delete mode 100644 src/main/deploy/basic_robot/drive_modules/backright.json delete mode 100644 src/main/deploy/basic_robot/drive_modules/frontleft.json delete mode 100644 src/main/deploy/basic_robot/drive_modules/frontright.json delete mode 100644 src/main/deploy/basic_robot/field/game_pieces.json delete mode 100644 src/main/deploy/basic_robot/robot.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example/percent_motor.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example/velocity_motor.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example/yams_arm.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example/yams_elevator.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example/yams_pivot.json delete mode 100644 src/main/deploy/basic_robot/subsystems/example/yams_shooter.json delete mode 100644 src/main/deploy/basic_robot/subsystems/led_strip.json delete mode 100644 src/main/deploy/basic_robot/yagsl_drivetrain.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/controllerproperties.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/modules/backleft.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/modules/backright.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/modules/frontleft.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/modules/frontright.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/modules/physicalproperties.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/modules/pidfproperties.json delete mode 100644 src/main/deploy/basic_robot/yagsl_swerve/swervedrive.json delete mode 100644 src/main/java/frc/robot/baby_swerve/BabySwerve.java delete mode 100644 src/main/java/frc/robot/example/ConfiguredMechanisms.java delete mode 100644 src/main/java/frc/robot/example/DisplayValueSubsystem.java delete mode 100644 src/main/java/frc/robot/example/ExampleRobot.java delete mode 100644 src/main/java/frc/robot/example/commands/ExampleCommands.java delete mode 100644 src/main/java/frc/robot/example/subsystems/ExampleIO.java delete mode 100644 src/main/java/frc/robot/example/subsystems/ExampleIOReal.java delete mode 100644 src/main/java/frc/robot/example/subsystems/ExampleIOSim.java delete mode 100644 src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java diff --git a/src/main/deploy/baby_swerve/cameras.json b/src/main/deploy/baby_swerve/cameras.json deleted file mode 100644 index bfdc48b6..00000000 --- a/src/main/deploy/baby_swerve/cameras.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cameras": [] -} diff --git a/src/main/deploy/baby_swerve/cameras/localization.json b/src/main/deploy/baby_swerve/cameras/localization.json deleted file mode 100644 index 50c5f575..00000000 --- a/src/main/deploy/baby_swerve/cameras/localization.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "two", - "use": "apriltag", - "type": "limelight", - "strategy": "LOWEST_AMBIGUITY", - "column": 0, - "x": 0.16, - "y": -0.1, - "z": 0.335, - "roll": -2, - "pitch": 0, - "yaw": 0 -} diff --git a/src/main/deploy/baby_swerve/competition_mode.json b/src/main/deploy/baby_swerve/competition_mode.json deleted file mode 100644 index f67b949b..00000000 --- a/src/main/deploy/baby_swerve/competition_mode.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "maxSpeed": 6.0, - "maxAngularSpeed": 6.0, - "maxAccelleration": 3.0, - "maxAngularAccelleration": 18.75 -} diff --git a/src/main/deploy/baby_swerve/controllers.json b/src/main/deploy/baby_swerve/controllers.json deleted file mode 100644 index 901f9478..00000000 --- a/src/main/deploy/baby_swerve/controllers.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "controllers": [ - "driver.json" - ] -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/driver_left_trigger.json b/src/main/deploy/baby_swerve/controllers/axis/driver_left_trigger.json deleted file mode 100644 index 41b79133..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/driver_left_trigger.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 2, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/driver_left_x.json b/src/main/deploy/baby_swerve/controllers/axis/driver_left_x.json deleted file mode 100644 index d77f0e91..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/driver_left_x.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 0, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/driver_left_y.json b/src/main/deploy/baby_swerve/controllers/axis/driver_left_y.json deleted file mode 100644 index d826731e..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/driver_left_y.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 1, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/driver_right_trigger.json b/src/main/deploy/baby_swerve/controllers/axis/driver_right_trigger.json deleted file mode 100644 index 47b46917..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/driver_right_trigger.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 3, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/driver_right_x.json b/src/main/deploy/baby_swerve/controllers/axis/driver_right_x.json deleted file mode 100644 index 8f981e23..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/driver_right_x.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 4, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/operator_left_y.json b/src/main/deploy/baby_swerve/controllers/axis/operator_left_y.json deleted file mode 100644 index c13bd0e0..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/operator_left_y.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "channel": 1, - "deadband": 0.07, - "invert": "true", - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0, - "rate": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/axis/operator_right_y.json b/src/main/deploy/baby_swerve/controllers/axis/operator_right_y.json deleted file mode 100644 index 2668956f..00000000 --- a/src/main/deploy/baby_swerve/controllers/axis/operator_right_y.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "channel": 5, - "deadband": 0.07, - "invert": "true", - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0, - "rate": 1.0 -} diff --git a/src/main/deploy/baby_swerve/controllers/driver.json b/src/main/deploy/baby_swerve/controllers/driver.json deleted file mode 100644 index e2dbc76a..00000000 --- a/src/main/deploy/baby_swerve/controllers/driver.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "driver", - "port": 0, - "axis": [ - "driver_left_y.json", - "driver_left_x.json", - "driver_right_x.json", - "driver_left_trigger.json", - "driver_right_trigger.json" - ] -} diff --git a/src/main/deploy/baby_swerve/controllers/operator.json b/src/main/deploy/baby_swerve/controllers/operator.json deleted file mode 100644 index 858cfea8..00000000 --- a/src/main/deploy/baby_swerve/controllers/operator.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "operator", - "port": 1, - "axis": [ - "operator_left_y.json", - "operator_right_y.json" - ] -} diff --git a/src/main/deploy/baby_swerve/demo_mode.json b/src/main/deploy/baby_swerve/demo_mode.json deleted file mode 100644 index 967ff73b..00000000 --- a/src/main/deploy/baby_swerve/demo_mode.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "maxSpeed": 0.5, - "maxAngularSpeed": 1.5, - "maxAccelleration": 0.5, - "maxAngularAccelleration": 1.5 -} diff --git a/src/main/deploy/baby_swerve/drive_modules/backleft.json b/src/main/deploy/baby_swerve/drive_modules/backleft.json deleted file mode 100644 index c62ce932..00000000 --- a/src/main/deploy/baby_swerve/drive_modules/backleft.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.19542, - "v": 2.2924, - "a": 0.35934 -} diff --git a/src/main/deploy/baby_swerve/drive_modules/backright.json b/src/main/deploy/baby_swerve/drive_modules/backright.json deleted file mode 100644 index aee7fd95..00000000 --- a/src/main/deploy/baby_swerve/drive_modules/backright.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.18096, - "v": 2.2915, - "a": 0.37156 -} diff --git a/src/main/deploy/baby_swerve/drive_modules/frontleft.json b/src/main/deploy/baby_swerve/drive_modules/frontleft.json deleted file mode 100644 index bdf7d3fa..00000000 --- a/src/main/deploy/baby_swerve/drive_modules/frontleft.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.21848, - "v": 2.3118, - "a": 0.20314 -} diff --git a/src/main/deploy/baby_swerve/drive_modules/frontright.json b/src/main/deploy/baby_swerve/drive_modules/frontright.json deleted file mode 100644 index 36a1c209..00000000 --- a/src/main/deploy/baby_swerve/drive_modules/frontright.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.18434, - "v": 2.3018, - "a": 0.30992 -} diff --git a/src/main/deploy/baby_swerve/robot.json b/src/main/deploy/baby_swerve/robot.json deleted file mode 100644 index 67726cd4..00000000 --- a/src/main/deploy/baby_swerve/robot.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "userConfig": "demo_mode.json", - "driveType": "YAGSL_SWERVE_DRIVE", - "trackWidth": 7.5, - "trackWidthUom": "in", - "wheelBase": 7.5, - "wheelBaseUom": "in", - "wheelDiameter": 3.1242, - "wheelDiameterUom": "in", - "physicalMaxSpeed": 5.93, - "physicalMaxSpeedUom": "m/s", - "driveMotorGearRatio": 1.0 -} diff --git a/src/main/deploy/baby_swerve/yagsl_drivetrain.json b/src/main/deploy/baby_swerve/yagsl_drivetrain.json deleted file mode 100644 index c75b4305..00000000 --- a/src/main/deploy/baby_swerve/yagsl_drivetrain.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "directory": "baby_swerve/yagsl_swerve", - "turningMotorGearRatio": 1.0, - "driveModules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ] -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/controllerproperties.json b/src/main/deploy/baby_swerve/yagsl_swerve/controllerproperties.json deleted file mode 100644 index c5ab6446..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/controllerproperties.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "angleJoystickRadiusDeadband": 0.5, - "heading": { - "p": 0.4, - "i": 0, - "d": 0.01 - } -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/backleft.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/backleft.json deleted file mode 100644 index 929f3fc9..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/backleft.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 3, - "canbus": null - }, - "angle": { - "type": "neo550", - "id": 4, - "canbus": null - }, - "encoder": { - "type": "thrifty", - "id": 1, - "canbus": null - }, - "inverted": { - "drive": false, - "angle": true - }, - "absoluteEncoderOffset": -120.6, - "absoluteEncoderInverted": true, - "location": { - "front": -3.75, - "left": 3.75 - } -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/backright.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/backright.json deleted file mode 100644 index a985764b..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/backright.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 5, - "canbus": null - }, - "angle": { - "type": "neo550", - "id": 6, - "canbus": null - }, - "encoder": { - "type": "thrifty", - "id": 2, - "canbus": null - }, - "inverted": { - "drive": false, - "angle": true - }, - "absoluteEncoderOffset": -166.15, - "absoluteEncoderInverted": true, - "location": { - "front": -3.75, - "left": -3.75 - } -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/frontleft.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/frontleft.json deleted file mode 100644 index 517bdf23..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/frontleft.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 1, - "canbus": null - }, - "angle": { - "type": "neo550", - "id": 2, - "canbus": null - }, - "encoder": { - "type": "thrifty", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": false, - "angle": true - }, - "absoluteEncoderOffset": -165.75, - "absoluteEncoderInverted": true, - "location": { - "front": 3.75, - "left": 3.75 - } -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/frontright.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/frontright.json deleted file mode 100644 index 7234a3ef..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/frontright.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 7, - "canbus": null - }, - "angle": { - "type": "neo550", - "id": 8, - "canbus": null - }, - "encoder": { - "type": "thrifty", - "id": 3, - "canbus": null - }, - "inverted": { - "drive": false, - "angle": true - }, - "absoluteEncoderOffset": -358.5, - "absoluteEncoderInverted": true, - "location": { - "front": 3.75, - "left": -3.75 - } -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/physicalproperties.json deleted file mode 100644 index 2f0bd03d..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/physicalproperties.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "conversionFactors": { - "angle": { - "gearRatio": 55.965, - "factor": 0 - }, - "drive": { - "gearRatio": 5.25, - "diameter": 3.1242, - "factor": 0 - } - }, - "currentLimit": { - "drive": 40, - "angle": 20 - }, - "rampRate": { - "drive": 0.1, - "angle": 0.1 - }, - "wheelGripCoefficientOfFriction": 1.19, - "optimalVoltage": 12, - "robotMass": 47.8 -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json deleted file mode 100644 index 735cb6b4..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/modules/pidfproperties.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "drive": { - "p": 0.00057373, - "i": 0, - "d": 0, - "f": 0, - "iz": 0 - }, - "angle": { - "p": 0.5, - "i": 0.000002, - "d": 0, - "f": 0, - "iz": 5 - } -} diff --git a/src/main/deploy/baby_swerve/yagsl_swerve/swervedrive.json b/src/main/deploy/baby_swerve/yagsl_swerve/swervedrive.json deleted file mode 100644 index 5fb01c6b..00000000 --- a/src/main/deploy/baby_swerve/yagsl_swerve/swervedrive.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "imu": { - "type": "navx", - "id": 0, - "canbus": null - }, - "invertedIMU": false, - "modules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ] -} diff --git a/src/main/deploy/basic_robot/akit_swerve_drivetrain.json b/src/main/deploy/basic_robot/akit_swerve_drivetrain.json deleted file mode 100644 index f1a74ad6..00000000 --- a/src/main/deploy/basic_robot/akit_swerve_drivetrain.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "type": "TalonFX", - "constants": { - "trackWidth": { - "val": 22, - "uom": "inches" - }, - "wheelBase": { - "val": 22, - "uom": "inches" - }, - "wheelDiameter": { - "val": 3.955, - "uom": "inches" - }, - "maxDriveSpeed": { - "val": 4.69, - "uom": "m/sec" - }, - "bumperFrameLength": { - "val": 30, - "uom": "inches" - }, - "bumperFrameWidth": { - "val": 30, - "uom": "inches" - }, - "gyro": { - "type": "pigeon2", - "id": 50, - "inverted": false, - "canbus": "" - }, - "driveGearRatio": "1:6.75", - "steerGearRatio": "1:25", - "driveMotorControl": { - "feedBack": { - "p": 0.00001, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0698163127444402, - "v": 0.11987949085405275, - "a": 0.0 - } - }, - "steerMotorControl": { - "feedBack": { - "p": 100, - "i": 0.0, - "d": 0.5 - }, - "feedForward": { - "s": 0.06242725612073021, - "v": 0.4513057312290134, - "a": 0.0 - } - }, - "modules": { - "frontLeft": { - "driveMotorSetup": { - "name": "frontLeftDrive", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 4 - }, - "steerMotorSetup": { - "name": "frontLeftSteer", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 3 - }, - "encoderId": 13, - "absoluteOffset": { - "val": 0.15234375, - "uom": "rotations" - }, - "encoderInverted": false - }, - "frontRight": { - "driveMotorSetup": { - "name": "frontRightDrive", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 2 - }, - "steerMotorSetup": { - "name": "frontRightSteer", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 1 - }, - "encoderId": 16, - "absoluteOffset": { - "val": -0.4873046875, - "uom": "rotations" - }, - "encoderInverted": false - }, - "backLeft": { - "driveMotorSetup": { - "name": "backLeftDrive", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 6 - }, - "steerMotorSetup": { - "name": "backLeftSteer", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 5 - }, - "encoderId": 14, - "absoluteOffset": { - "val": -0.219482421875, - "uom": "rotations" - }, - "encoderInverted": false - }, - "backRight": { - "driveMotorSetup": { - "name": "backRightDrive", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 8 - }, - "steerMotorSetup": { - "name": "backRightSteer", - "controllerType": "talonfx", - "motorType": "Krakenx60", - "canId": 7 - }, - "encoderId": 15, - "absoluteOffset": { - "val": 0.17236328125, - "uom": "rotations" - }, - "encoderInverted": false - } - }, - "coupleRatio": 3.8181818181818183, - "invertLeftSide": true, - "invertRightSide": true, - "steerInertia": { - "val": 0.04, - "uom": "kg*m^2" - }, - "driveInertia": { - "val": 0.025, - "uom": "kg*m^2" - }, - "robotMass": { - "val": 50, - "uom": "kg" - }, - "wheelCOF": 1.2, - "slipCurrent": { - "val": 120, - "uom": "amps" - }, - "canbus": "", - "startingPose": { - "x": { - "val": 3.42, - "uom": "m" - }, - "y": { - "val": 5.75, - "uom": "m" - }, - "rotation": { - "val": -45, - "uom": "deg" - } - } - } -} diff --git a/src/main/deploy/basic_robot/cameras.json b/src/main/deploy/basic_robot/cameras.json deleted file mode 100644 index ac73ba18..00000000 --- a/src/main/deploy/basic_robot/cameras.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "cameras": [ - "left.json", - "right.json", - "quest.json" - ], - "viewGamePieces": false, - "aprilTagLayout": "2026-rebuilt-andymark.json" -} diff --git a/src/main/deploy/basic_robot/cameras/intake.json b/src/main/deploy/basic_robot/cameras/intake.json deleted file mode 100644 index 33938add..00000000 --- a/src/main/deploy/basic_robot/cameras/intake.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "intake", - "use": "target", - "type": "limelight", - "column": 0, - "x": 0.15, - "y": 0.05, - "z": 0.2, - "roll": 0, - "pitch": 10, - "yaw": 0, - "targetHeight": 0.02 -} diff --git a/src/main/deploy/basic_robot/cameras/left.json b/src/main/deploy/basic_robot/cameras/left.json deleted file mode 100644 index 0eaef281..00000000 --- a/src/main/deploy/basic_robot/cameras/left.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "left", - "use": "apriltag", - "type": "photonvision", - "column": 0, - "x": -0.33, - "y": 0.33, - "z": 0.5, - "roll": 0, - "pitch": 0, - "yaw": 90 -} diff --git a/src/main/deploy/basic_robot/cameras/localization.json b/src/main/deploy/basic_robot/cameras/localization.json deleted file mode 100644 index 481de59d..00000000 --- a/src/main/deploy/basic_robot/cameras/localization.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "localization", - "use": "apriltag", - "type": "limelight", - "column": 0, - "x": -0.35, - "y": -0.25, - "z": 0.5, - "roll": 0, - "pitch": -20, - "yaw": 180 -} diff --git a/src/main/deploy/basic_robot/cameras/quest.json b/src/main/deploy/basic_robot/cameras/quest.json deleted file mode 100644 index df412311..00000000 --- a/src/main/deploy/basic_robot/cameras/quest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "quest", - "use": "quest", - "column": 0, - "x": 0, - "y": 0, - "z": 18, - "roll": 0, - "pitch": 0, - "yaw": 0 -} diff --git a/src/main/deploy/basic_robot/cameras/right.json b/src/main/deploy/basic_robot/cameras/right.json deleted file mode 100644 index aed9a771..00000000 --- a/src/main/deploy/basic_robot/cameras/right.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "right", - "use": "apriltag", - "type": "photonvision", - "column": 0, - "x": -0.33, - "y": -0.33, - "z": 0.5, - "roll": 0, - "pitch": 0, - "yaw": -90 -} diff --git a/src/main/deploy/basic_robot/cameras/shooter.json b/src/main/deploy/basic_robot/cameras/shooter.json deleted file mode 100644 index 9cf3bb35..00000000 --- a/src/main/deploy/basic_robot/cameras/shooter.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "shooter", - "use": "target", - "type": "photonvision", - "column": 0, - "x": 0.25, - "y": 0, - "z": 0.25, - "roll": 0, - "pitch": 10, - "yaw": 0, - "targetFiducialIds": [ - 17, - 18, - 19, - 20, - 21, - 22 - ] -} diff --git a/src/main/deploy/basic_robot/competition_mode.json b/src/main/deploy/basic_robot/competition_mode.json deleted file mode 100644 index f67b949b..00000000 --- a/src/main/deploy/basic_robot/competition_mode.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "maxSpeed": 6.0, - "maxAngularSpeed": 6.0, - "maxAccelleration": 3.0, - "maxAngularAccelleration": 18.75 -} diff --git a/src/main/deploy/basic_robot/controllers.json b/src/main/deploy/basic_robot/controllers.json deleted file mode 100644 index c6c86acb..00000000 --- a/src/main/deploy/basic_robot/controllers.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "controllers": [ - "driver.json", - "operator.json" - ] -} diff --git a/src/main/deploy/basic_robot/controllers/axis/driver_left_trigger.json b/src/main/deploy/basic_robot/controllers/axis/driver_left_trigger.json deleted file mode 100644 index 41b79133..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/driver_left_trigger.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 2, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/axis/driver_left_x.json b/src/main/deploy/basic_robot/controllers/axis/driver_left_x.json deleted file mode 100644 index d77f0e91..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/driver_left_x.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 0, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/axis/driver_left_y.json b/src/main/deploy/basic_robot/controllers/axis/driver_left_y.json deleted file mode 100644 index d826731e..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/driver_left_y.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 1, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/axis/driver_right_trigger.json b/src/main/deploy/basic_robot/controllers/axis/driver_right_trigger.json deleted file mode 100644 index 47b46917..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/driver_right_trigger.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 3, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/axis/driver_right_x.json b/src/main/deploy/basic_robot/controllers/axis/driver_right_x.json deleted file mode 100644 index 8f981e23..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/driver_right_x.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "channel": 4, - "deadband": 0.07, - "invert": true, - "scale": 1.0, - "curvePower": 3, - "limit": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/axis/operator_left_y.json b/src/main/deploy/basic_robot/controllers/axis/operator_left_y.json deleted file mode 100644 index c13bd0e0..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/operator_left_y.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "channel": 1, - "deadband": 0.07, - "invert": "true", - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0, - "rate": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/axis/operator_right_y.json b/src/main/deploy/basic_robot/controllers/axis/operator_right_y.json deleted file mode 100644 index 2668956f..00000000 --- a/src/main/deploy/basic_robot/controllers/axis/operator_right_y.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "channel": 5, - "deadband": 0.07, - "invert": "true", - "scale": 1.0, - "curvePower": 3.0, - "limit": 1.0, - "rate": 1.0 -} diff --git a/src/main/deploy/basic_robot/controllers/driver.json b/src/main/deploy/basic_robot/controllers/driver.json deleted file mode 100644 index e2dbc76a..00000000 --- a/src/main/deploy/basic_robot/controllers/driver.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "driver", - "port": 0, - "axis": [ - "driver_left_y.json", - "driver_left_x.json", - "driver_right_x.json", - "driver_left_trigger.json", - "driver_right_trigger.json" - ] -} diff --git a/src/main/deploy/basic_robot/controllers/operator.json b/src/main/deploy/basic_robot/controllers/operator.json deleted file mode 100644 index 858cfea8..00000000 --- a/src/main/deploy/basic_robot/controllers/operator.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "operator", - "port": 1, - "axis": [ - "operator_left_y.json", - "operator_right_y.json" - ] -} diff --git a/src/main/deploy/basic_robot/demo_mode.json b/src/main/deploy/basic_robot/demo_mode.json deleted file mode 100644 index f60f73c9..00000000 --- a/src/main/deploy/basic_robot/demo_mode.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "maxSpeed": 2.0, - "maxAngularSpeed": 2.0, - "maxAccelleration": 1.0, - "maxAngularAccelleration": 9.0 -} diff --git a/src/main/deploy/basic_robot/drive_modules/backleft.json b/src/main/deploy/basic_robot/drive_modules/backleft.json deleted file mode 100644 index c62ce932..00000000 --- a/src/main/deploy/basic_robot/drive_modules/backleft.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.19542, - "v": 2.2924, - "a": 0.35934 -} diff --git a/src/main/deploy/basic_robot/drive_modules/backright.json b/src/main/deploy/basic_robot/drive_modules/backright.json deleted file mode 100644 index aee7fd95..00000000 --- a/src/main/deploy/basic_robot/drive_modules/backright.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.18096, - "v": 2.2915, - "a": 0.37156 -} diff --git a/src/main/deploy/basic_robot/drive_modules/frontleft.json b/src/main/deploy/basic_robot/drive_modules/frontleft.json deleted file mode 100644 index bdf7d3fa..00000000 --- a/src/main/deploy/basic_robot/drive_modules/frontleft.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.21848, - "v": 2.3118, - "a": 0.20314 -} diff --git a/src/main/deploy/basic_robot/drive_modules/frontright.json b/src/main/deploy/basic_robot/drive_modules/frontright.json deleted file mode 100644 index 36a1c209..00000000 --- a/src/main/deploy/basic_robot/drive_modules/frontright.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "s": 0.18434, - "v": 2.3018, - "a": 0.30992 -} diff --git a/src/main/deploy/basic_robot/field/game_pieces.json b/src/main/deploy/basic_robot/field/game_pieces.json deleted file mode 100644 index a1e2a1fb..00000000 --- a/src/main/deploy/basic_robot/field/game_pieces.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gamePieces": [ - { - "x": 2, - "y": 7, - "type": "Fuel", - "rotation": 0 - } - ] -} diff --git a/src/main/deploy/basic_robot/robot.json b/src/main/deploy/basic_robot/robot.json deleted file mode 100644 index 2475d0f1..00000000 --- a/src/main/deploy/basic_robot/robot.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "userConfig": "competition_mode.json", - "driveType": "AKIT_SWERVE_DRIVE", - "trackWidth": 16.8, - "trackWidthUom": "in", - "wheelBase": 18.5, - "wheelBaseUom": "in", - "wheelDiameter": 0.103386904, - "wheelDiameterUom": "m", - "physicalMaxSpeed": 5.93, - "physicalMaxSpeedUom": "m/s", - "driveMotorGearRatio": 1.0, - "loadSimulatedField": true, - "gamePieceA": "Fuel" -} diff --git a/src/main/deploy/basic_robot/subsystems/example.json b/src/main/deploy/basic_robot/subsystems/example.json deleted file mode 100644 index 91fb54bd..00000000 --- a/src/main/deploy/basic_robot/subsystems/example.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "devices": [ - { - "device": "percent_motor", - "file": "example/percent_motor.json" - }, - { - "device": "velocity_motor", - "file": "example/velocity_motor.json" - }, - { - "device": "yams_shooter", - "file": "example/yams_shooter.json" - }, - { - "device": "yams_arm", - "file": "example/yams_arm.json" - }, - { - "device": "yams_turret", - "file": "example/yams_pivot.json" - } - ] -} diff --git a/src/main/deploy/basic_robot/subsystems/example/percent_motor.json b/src/main/deploy/basic_robot/subsystems/example/percent_motor.json deleted file mode 100644 index 93be5418..00000000 --- a/src/main/deploy/basic_robot/subsystems/example/percent_motor.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "percent_motor", - "controller": "spark", - "type": "Neo", - "id": 11, - "gearing": 1.0, - "momentOfInertiaKgMSq": 1.0, - "x": 0.5, - "y": -0.092, - "z": 0.2 -} diff --git a/src/main/deploy/basic_robot/subsystems/example/velocity_motor.json b/src/main/deploy/basic_robot/subsystems/example/velocity_motor.json deleted file mode 100644 index 185de2e0..00000000 --- a/src/main/deploy/basic_robot/subsystems/example/velocity_motor.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "velocity_motor", - "controller": "spark", - "type": "Neo", - "id": 14, - "gearing": 1.0, - "momentOfInertiaKgMSq": 1.0, - "x": -0.25, - "y": 0.0, - "z": 0.25, - "kP": 0.1, - "kI": 0.0, - "kD": 0.01, - "iZone": 0.0, - "kS": 0.1, - "kV": 0.1, - "kA": 0.0 -} diff --git a/src/main/deploy/basic_robot/subsystems/example/yams_arm.json b/src/main/deploy/basic_robot/subsystems/example/yams_arm.json deleted file mode 100644 index 2f588f40..00000000 --- a/src/main/deploy/basic_robot/subsystems/example/yams_arm.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "motorSetup": { - "name": "Hood", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 13, - "robotToMotor": { - "x": { - "val": -8.2, - "uom": "in" - }, - "y": { - "val": 10.104, - "uom": "in" - }, - "z": { - "val": 15.786, - "uom": "in" - }, - "rotation": { - "val": 0.0, - "uom": "deg" - } - } - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "simSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "length": { - "val": 0.234, - "uom": "m" - }, - "lowerHardLimit": { - "val": 55, - "uom": "deg" - }, - "upperHardLimit": { - "val": 100, - "uom": "deg" - }, - "startingAngle": { - "val": 55, - "uom": "deg" - }, - "lowerSoftLimit": { - "val": 55, - "uom": "deg" - }, - "upperSoftLimit": { - "val": 85, - "uom": "deg" - }, - "gearing": [ - 3, - 4 - ], - "mass": { - "val": 1, - "uom": "lbs" - }, - "horizontalZero": { - "val": 55, - "uom": "deg" - } -} diff --git a/src/main/deploy/basic_robot/subsystems/example/yams_elevator.json b/src/main/deploy/basic_robot/subsystems/example/yams_elevator.json deleted file mode 100644 index e8cbe550..00000000 --- a/src/main/deploy/basic_robot/subsystems/example/yams_elevator.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "motorSetup": { - "name": "Elevator", - "controllerType": "spark", - "motorType": "Neo", - "canId": 10, - "robotToMotor": { - "x": { - "val": -10.7, - "uom": "in" - }, - "y": { - "val": -3.669, - "uom": "in" - }, - "z": { - "val": 5.323, - "uom": "in" - }, - "rotation": { - "val": 0.0, - "uom": "deg" - } - } - }, - "motorSystemId": { - "feedBack": { - "p": 2.7739E-10, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.013799, - "g": 0.24168, - "v": 10.219, - "a": 0.2153 - }, - "maxVelocity": { - "val": 4, - "uom": "m/s" - }, - "maxAcceleration": { - "val": 20, - "uom": "m/s^2" - }, - "controlMode": "CLOSED_LOOP" - }, - "simSystemId": { - "feedBack": { - "p": 2.7739E-10, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.013799, - "g": 0.24168, - "v": 10.219, - "a": 0.2153 - }, - "maxVelocity": { - "val": 4, - "uom": "m/s" - }, - "maxAcceleration": { - "val": 20, - "uom": "m/s^2" - } - }, - "sprocketTeeth": 22, - "lowerSoftLimit": { - "val": 0.1, - "uom": "m" - }, - "upperSoftLimit": { - "val": 2, - "uom": "m" - }, - "lowerHardLimit": { - "val": 0.1, - "uom": "m" - }, - "upperHardLimit": { - "val": 3, - "uom": "m" - }, - "gearing": [ - 3, - 4 - ], - "startingPosition": { - "val": 0.5, - "uom": "m" - }, - "mass": { - "val": 16, - "uom": "lbs" - } -} diff --git a/src/main/deploy/basic_robot/subsystems/example/yams_pivot.json b/src/main/deploy/basic_robot/subsystems/example/yams_pivot.json deleted file mode 100644 index d3d3b2ba..00000000 --- a/src/main/deploy/basic_robot/subsystems/example/yams_pivot.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "motorSetup": { - "name": "Turret", - "controllerType": "talonfx", - "motorType": "KrakenX60", - "canId": 15, - "robotToMotor": { - "x": { - "val": -5.872, - "uom": "in" - }, - "y": { - "val": 4.8, - "uom": "in" - }, - "z": { - "val": 14.466, - "uom": "in" - }, - "rotation": { - "val": 0.0, - "uom": "deg" - } - }, - "movementPlane": "XY" - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "simSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - }, - "maxVelocity": { - "val": 180, - "uom": "deg/s" - }, - "maxAcceleration": { - "val": 90, - "uom": "deg/s^2" - } - }, - "lowerHardLimit": { - "val": -165, - "uom": "deg" - }, - "upperHardLimit": { - "val": 165, - "uom": "deg" - }, - "startingAngle": { - "val": 0, - "uom": "deg" - }, - "lowerSoftLimit": { - "val": -160, - "uom": "deg" - }, - "upperSoftLimit": { - "val": 160, - "uom": "deg" - }, - "gearStages": "12:36:40:12:108", - "radius": { - "val": 5, - "uom": "in" - }, - "mass": { - "val": 20, - "uom": "lb" - } -} diff --git a/src/main/deploy/basic_robot/subsystems/example/yams_shooter.json b/src/main/deploy/basic_robot/subsystems/example/yams_shooter.json deleted file mode 100644 index dc9a2c3b..00000000 --- a/src/main/deploy/basic_robot/subsystems/example/yams_shooter.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "motorSetup": { - "name": "Shooter", - "controllerType": "spark", - "motorType": "KrakenX44", - "canId": 12, - "robotToMotor": { - "x": { - "val": -5.872, - "uom": "in" - }, - "y": { - "val": 4.8, - "uom": "in" - }, - "z": { - "val": 18.72, - "uom": "in" - }, - "rotation": { - "val": 0.0, - "uom": "deg" - } - } - }, - "motorSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - } - }, - "simSystemId": { - "feedBack": { - "p": 4, - "i": 0.0, - "d": 0.0 - }, - "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 - } - }, - "lowerSoftLimit": { - "val": 0, - "uom": "rpm" - }, - "upperSoftLimit": { - "val": 5000, - "uom": "rpm" - }, - "gearing": [ - 3, - 4 - ], - "mass": { - "val": 2, - "uom": "kg" - }, - "radius": { - "val": 0.05, - "uom": "m" - } -} diff --git a/src/main/deploy/basic_robot/subsystems/led_strip.json b/src/main/deploy/basic_robot/subsystems/led_strip.json deleted file mode 100644 index 25c307af..00000000 --- a/src/main/deploy/basic_robot/subsystems/led_strip.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "length": 30, - "dataPin": 0, - "segments": [ - { - "name": "status_indicator", - "start": 0, - "end": 9 - }, - { - "name": "team_colors", - "start": 10, - "end": 29 - } - ] -} diff --git a/src/main/deploy/basic_robot/yagsl_drivetrain.json b/src/main/deploy/basic_robot/yagsl_drivetrain.json deleted file mode 100644 index c6beb5d7..00000000 --- a/src/main/deploy/basic_robot/yagsl_drivetrain.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "directory": "basic_robot/yagsl_swerve", - "turningMotorGearRatio": 1.0, - "driveModules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ], - "startingPose": { - "x": { - "val": 1.0, - "uom": "m" - }, - "y": { - "val": 1.0, - "uom": "m" - }, - "rotation": { - "val": 0.0, - "uom": "deg" - } - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/controllerproperties.json b/src/main/deploy/basic_robot/yagsl_swerve/controllerproperties.json deleted file mode 100644 index dd353caf..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/controllerproperties.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "angleJoystickRadiusDeadband": 0.05, - "heading": { - "p": 0.4, - "i": 0, - "d": 0.01 - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/basic_robot/yagsl_swerve/modules/backleft.json deleted file mode 100644 index 2ac2a1ab..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/modules/backleft.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 17, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 6, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": -8.37, - "left": 10.88 - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/basic_robot/yagsl_swerve/modules/backright.json deleted file mode 100644 index eea317b8..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/modules/backright.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 18, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 2, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": -8.37, - "left": -10.88 - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/basic_robot/yagsl_swerve/modules/frontleft.json deleted file mode 100644 index 1c03c3f1..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/modules/frontleft.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 15, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 4, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": 8.37, - "left": 10.88 - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/basic_robot/yagsl_swerve/modules/frontright.json deleted file mode 100644 index 6218565f..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/modules/frontright.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "drive": { - "type": "neo", - "id": 16, - "canbus": null - }, - "angle": { - "type": "neo", - "id": 3, - "canbus": null - }, - "encoder": { - "type": "canandcoder", - "id": 0, - "canbus": null - }, - "inverted": { - "drive": true, - "angle": true - }, - "absoluteEncoderInverted": true, - "location": { - "front": 8.37, - "left": -10.88 - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/modules/physicalproperties.json b/src/main/deploy/basic_robot/yagsl_swerve/modules/physicalproperties.json deleted file mode 100644 index c921b000..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/modules/physicalproperties.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "conversionFactors": { - "angle": { - "gearRatio": 12.8, - "factor": 0 - }, - "drive": { - "gearRatio": 8.14, - "diameter": 4, - "factor": 0 - } - }, - "currentLimit": { - "drive": 100, - "angle": 20 - }, - "rampRate": { - "drive": 0.15, - "angle": 0.05 - }, - "wheelGripCoefficientOfFriction": 1.19, - "optimalVoltage": 12, - "robotMass": 75 -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/basic_robot/yagsl_swerve/modules/pidfproperties.json deleted file mode 100644 index 5924fdbb..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/modules/pidfproperties.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "drive": { - "p": 0.00023, - "i": 0.0000002, - "d": 1, - "f": 0, - "iz": 0 - }, - "angle": { - "p": 0.0020645, - "i": 0, - "d": 0, - "f": 0.001, - "iz": 0 - } -} diff --git a/src/main/deploy/basic_robot/yagsl_swerve/swervedrive.json b/src/main/deploy/basic_robot/yagsl_swerve/swervedrive.json deleted file mode 100644 index f4876233..00000000 --- a/src/main/deploy/basic_robot/yagsl_swerve/swervedrive.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "imu": { - "type": "pigeon2", - "id": 13, - "canbus": "" - }, - "invertedIMU": false, - "modules": [ - "frontleft.json", - "frontright.json", - "backleft.json", - "backright.json" - ] -} diff --git a/src/main/java/frc/robot/baby_swerve/BabySwerve.java b/src/main/java/frc/robot/baby_swerve/BabySwerve.java deleted file mode 100644 index ae8be2a3..00000000 --- a/src/main/java/frc/robot/baby_swerve/BabySwerve.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.baby_swerve; - -import edu.wpi.first.wpilibj2.command.Command; -import frc.robot.example.DisplayValueSubsystem; -import org.frc5010.common.arch.GenericRobot; -import org.frc5010.common.config.ConfigConstants; -import org.frc5010.common.constants.SwerveConstants; -import org.frc5010.common.drive.GenericDrivetrain; -import org.frc5010.common.sensors.Controller; - -/** This is an example robot class. */ -public class BabySwerve extends GenericRobot { - SwerveConstants swerveConstants; - GenericDrivetrain drivetrain; - DisplayValueSubsystem displayValueSubsystem = new DisplayValueSubsystem(); - - public BabySwerve(String directory) { - super(directory); - drivetrain = (GenericDrivetrain) getSubsystem(ConfigConstants.DRIVETRAIN); - } - - @Override - public void configureButtonBindings(Controller driver, Controller operator) {} - - @Override - public void setupDefaultCommands(Controller driver, Controller operator) { - drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); - } - - @Override - public void initAutoCommands() { - drivetrain.setAutoBuilder(); - } - - @Override - public Command generateAutoCommand(Command autoCommand) { - return drivetrain.generateAutoCommand(autoCommand); - } -} diff --git a/src/main/java/frc/robot/example/ConfiguredMechanisms.java b/src/main/java/frc/robot/example/ConfiguredMechanisms.java deleted file mode 100644 index 0082ec44..00000000 --- a/src/main/java/frc/robot/example/ConfiguredMechanisms.java +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example; - -import static edu.wpi.first.units.Units.Second; -import static edu.wpi.first.units.Units.Volts; - -import edu.wpi.first.units.measure.Angle; -import edu.wpi.first.units.measure.Distance; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.SubsystemBase; -import org.frc5010.common.config.json.devices.ArmParser; -import org.frc5010.common.config.json.devices.ElevatorParser; -import org.frc5010.common.config.json.devices.PivotParser; -import yams.mechanisms.positional.Arm; -import yams.mechanisms.positional.Elevator; -import yams.mechanisms.positional.Pivot; - -public class ConfiguredMechanisms extends SubsystemBase { - Elevator elevator; - Arm arm; // Assuming you have an Arm subsystem, otherwise remove this line - Pivot pivot; // Assuming you have a Pivot subsystem, otherwise remove this line - - /** Creates a new ConfiguredElevator. */ - public ConfiguredMechanisms() { - elevator = ElevatorParser.parse("mechanisms", "yams_elevator.json", this); - arm = ArmParser.parse("mechanisms", "yams_arm.json", this); - pivot = PivotParser.parse("mechanisms", "yams_pivot.json", this); - } - - public void periodic() { - elevator.updateTelemetry(); - arm.updateTelemetry(); - pivot.updateTelemetry(); - } - - public void simulationPeriodic() { - elevator.simIterate(); - arm.simIterate(); - pivot.simIterate(); - } - - public Command elevCmd(double dutycycle) { - return elevator.set(dutycycle); - } - - public Command setHeight(Distance height) { - return elevator.setHeight(height); - } - - public Command sysIdElevator() { - return elevator.sysId(Volts.of(12), Volts.of(12).per(Second), Second.of(30)); - } - - public Command armCmd(double dutycycle) { - return arm.set(dutycycle); - } - - public Command sysIdArm() { - return arm.sysId(Volts.of(3), Volts.of(3).per(Second), Second.of(30)); - } - - public Command setArmAngle(Angle angle) { - return arm.setAngle(angle); - } - - public Command turretCmd(double dutycycle) { - return pivot.set(dutycycle); - } - - public Command sysIdTurret() { - return pivot.sysId(Volts.of(3), Volts.of(3).per(Second), Second.of(30)); - } - - public Command setPivotAngle(Angle angle) { - return pivot.setAngle(angle); - } -} diff --git a/src/main/java/frc/robot/example/DisplayValueSubsystem.java b/src/main/java/frc/robot/example/DisplayValueSubsystem.java deleted file mode 100644 index c7207255..00000000 --- a/src/main/java/frc/robot/example/DisplayValueSubsystem.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example; - -import static edu.wpi.first.units.Units.Degrees; - -import org.frc5010.common.arch.GenericSubsystem; -import org.frc5010.common.telemetry.DisplayAngle; -import org.frc5010.common.telemetry.DisplayBoolean; -import org.frc5010.common.telemetry.DisplayDouble; -import org.frc5010.common.telemetry.DisplayFloat; -import org.frc5010.common.telemetry.DisplayLength; -import org.frc5010.common.telemetry.DisplayLong; -import org.frc5010.common.telemetry.DisplayString; -import org.frc5010.common.telemetry.DisplayTime; - -/** Tests the classes in the {@link org.frc5010.common.telemetry} package that Display values */ -public class DisplayValueSubsystem extends GenericSubsystem { - DisplayAngle inputAngle; - DisplayAngle outputAngle; - DisplayBoolean inputBoolean; - DisplayBoolean outputBoolean; - DisplayDouble inputDouble; - DisplayDouble outputDouble; - DisplayFloat inputFloat; - DisplayFloat outputFloat; - DisplayLength inputLength; - DisplayLength outputLength; - DisplayLong inputLong; - DisplayLong outputLong; - DisplayString inputString; - DisplayString outputString; - DisplayTime inputTime; - DisplayTime outputTime; - DisplayAngle outAngle; - - public DisplayValueSubsystem() { - super(); - outAngle = DashBoard.makeInfoAngle("Out Angle"); - outputAngle = DashBoard.makeDisplayAngle("OUTPUT_ANGLE"); - outputBoolean = DashBoard.makeDisplayBoolean("OUTPUT_BOOLEAN"); - outputDouble = DashBoard.makeDisplayDouble("OUTPUT_DOUBLE"); - outputFloat = DashBoard.makeDisplayFloat("OUTPUT_FLOAT"); - DashBoard.nextColumn("Config"); - inputAngle = DashBoard.makeConfigAngle("INPUT_ANGLE"); - inputBoolean = DashBoard.makeConfigBoolean("INPUT_BOOLEAN"); - inputDouble = DashBoard.makeConfigDouble("INPUT_DOUBLE"); - inputFloat = DashBoard.makeConfigFloat("INPUT_FLOAT"); - DashBoard.nextColumn("Input"); - inputLength = DashBoard.makeConfigLength("INPUT_LENGTH"); - inputLong = DashBoard.makeConfigLong("INPUT_LONG"); - inputString = DashBoard.makeConfigString("INPUT_STRING"); - inputTime = DashBoard.makeConfigTime("INPUT_TIME"); - DashBoard.nextColumn("Debug-Info"); - outputLength = DashBoard.makeInfoLength("OUTPUT_LENGTH"); - outputLong = DashBoard.makeInfoLong("OUTPUT_LONG"); - outputString = DashBoard.makeInfoString("OUTPUT_STRING"); - outputTime = DashBoard.makeInfoTime("OUTPUT_TIME"); - } - - @Override - public void periodic() { - // This method will be called once per scheduler run - outputAngle.setAngle(inputAngle); - outputBoolean.setValue(inputBoolean.getValue()); - outputDouble.setValue(inputDouble.getValue()); - outputFloat.setValue(inputFloat.getValue()); - outputLength.setLength(inputLength); - outputLong.setValue(inputLong.getValue()); - outputString.setValue(inputString.getValue()); - outputTime.setTime(inputTime); - outAngle.setAngle(Degrees.of(Math.random() * 360.0)); - } -} diff --git a/src/main/java/frc/robot/example/ExampleRobot.java b/src/main/java/frc/robot/example/ExampleRobot.java deleted file mode 100644 index 0c8873a9..00000000 --- a/src/main/java/frc/robot/example/ExampleRobot.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example; - -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import frc.robot.example.commands.ExampleCommands; -import frc.robot.example.subsystems.ExampleSubsystem; -import org.frc5010.common.arch.GenericRobot; -import org.frc5010.common.config.ConfigConstants; -import org.frc5010.common.constants.SwerveConstants; -import org.frc5010.common.drive.GenericDrivetrain; -import org.frc5010.common.sensors.Controller; - -/** This is an example robot class. */ -public class ExampleRobot extends GenericRobot { - SwerveConstants swerveConstants; - GenericDrivetrain drivetrain; - DisplayValueSubsystem displayValueSubsystem = new DisplayValueSubsystem(); - ExampleSubsystem exampleSubsystem; - ExampleCommands exampleCommands; - - public ExampleRobot(String directory) { - super(directory); - drivetrain = (GenericDrivetrain) subsystems.get(ConfigConstants.DRIVETRAIN); - exampleSubsystem = new ExampleSubsystem(); - exampleCommands = new ExampleCommands(subsystems); - } - - @Override - public void configureButtonBindings(Controller driver, Controller operator) { - exampleCommands.configureButtonBindings(driver, operator); - } - - @Override - public void setupDefaultCommands(Controller driver, Controller operator) { - exampleCommands.setDefaultCommands(driver, operator); - drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); - } - - @Override - public void initAutoCommands() { - drivetrain.setAutoBuilder(); - } - - @Override - public Command generateAutoCommand(Command autoCommand) { - return drivetrain.generateAutoCommand(autoCommand); - } - - @Override - public void buildAutoCommands() { - super.buildAutoCommands(); - selectableCommand.addOption("Do Nothing", Commands.none()); - drivetrain.addAutoCommands(selectableCommand); - } -} diff --git a/src/main/java/frc/robot/example/commands/ExampleCommands.java b/src/main/java/frc/robot/example/commands/ExampleCommands.java deleted file mode 100644 index 99bdb403..00000000 --- a/src/main/java/frc/robot/example/commands/ExampleCommands.java +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example.commands; - -import static edu.wpi.first.units.Units.Inches; - -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import frc.robot.example.subsystems.ExampleSubsystem; -import java.util.Map; -import org.frc5010.common.arch.GenericSubsystem; -import org.frc5010.common.arch.StateMachine; -import org.frc5010.common.arch.StateMachine.State; -import org.frc5010.common.config.ConfigConstants; -import org.frc5010.common.drive.GenericDrivetrain; -import org.frc5010.common.sensors.Controller; -import org.frc5010.common.telemetry.DisplayString; -import org.frc5010.common.telemetry.DisplayValuesHelper; - -/** Add your docs here. */ -public class ExampleCommands { - private StateMachine stateMachine; - private DisplayString commandState; - private DisplayValuesHelper DisplayHelper; - private State intakeState; - private State lowState; - private State prepState; - private State readyState; - private ExampleSubsystem launcher; - private GenericDrivetrain drivetrain; - private Map subsystems; - private Translation2d target = new Translation2d(Inches.of(182.11), Inches.of(158.84)); - - private static enum LauncherState { - INTAKE, - LOW_SPEED, - PREP_SHOOT, - READY_TO_SHOOT - } - - private LauncherState requestedState = LauncherState.INTAKE; - - public ExampleCommands(Map subsystems) { - this.subsystems = subsystems; - DisplayHelper = new DisplayValuesHelper("LauncherCommands", "Values"); - commandState = DisplayHelper.makeDisplayString("Launcher State"); - - launcher = (ExampleSubsystem) subsystems.get(ExampleSubsystem.class.getSimpleName()); - drivetrain = (GenericDrivetrain) this.subsystems.get(ConfigConstants.DRIVETRAIN); - - stateMachine = new StateMachine("LauncherStateMachine"); - intakeState = stateMachine.addState("INTAKE", intakeStateCommand()); - lowState = stateMachine.addState("LOW-SPEED", lowStateCommand()); - prepState = stateMachine.addState("PREP-SHOOT", prepStateCommand()); - readyState = stateMachine.addState("READY-TO-SHOOT", readyStateCommand()); - stateMachine.setInitialState(intakeState); - } - - public void setDefaultCommands(Controller driver, Controller operator) { - if (launcher != null) { - stateMachine.addRequirements(launcher); - launcher.setDefaultCommand(stateMachine); - } - } - - public void configureButtonBindings(Controller driver, Controller operator) { - driver.createRightBumper().onTrue(shouldPrepCommand()).onFalse(shouldIntakeCommand()); - driver.createLeftBumper().onTrue(shouldShootCommand()).onFalse(shouldPrepCommand()); - driver.createAButton().onTrue(shouldIntakeCommand()).onFalse(shouldUseLowSpeed()); - - driver.createBButton().whileTrue(launcher.sysIdPivot()); - - lowState.switchTo(prepState).when(() -> requestedState == LauncherState.PREP_SHOOT); - prepState.switchTo(lowState).when(() -> requestedState == LauncherState.LOW_SPEED); - - prepState.switchTo(readyState).when(() -> requestedState == LauncherState.READY_TO_SHOOT); - readyState.switchTo(prepState).when(() -> requestedState == LauncherState.PREP_SHOOT); - - intakeState.switchTo(lowState).when(() -> requestedState == LauncherState.LOW_SPEED); - lowState.switchTo(intakeState).when(() -> requestedState == LauncherState.INTAKE); - } - - public Command shouldPrepCommand() { - return Commands.runOnce(() -> requestedState = LauncherState.PREP_SHOOT); - } - - public Command shouldUseLowSpeed() { - return Commands.runOnce(() -> requestedState = LauncherState.LOW_SPEED); - } - - public Command shouldShootCommand() { - return Commands.runOnce(() -> requestedState = LauncherState.READY_TO_SHOOT); - } - - public Command shouldIntakeCommand() { - return Commands.runOnce(() -> requestedState = LauncherState.INTAKE); - } - - private Translation2d getTargetPose() { - return target.minus(drivetrain.getPoseEstimator().getCurrentPose().getTranslation()); - } - - private Command intakeStateCommand() { - return Commands.parallel( - Commands.runOnce(() -> commandState.setValue("Intake")), - launcher.stopTrackingCommand(), - launcher.intakeCommand()); - } - - private Command lowStateCommand() { - return Commands.parallel( - Commands.runOnce(() -> commandState.setValue("Low Speed")), - launcher.stopIntakeCommand(), - launcher.trackTargetCommand(() -> getTargetPose())); - } - - private Command prepStateCommand() { - return Commands.parallel( - Commands.runOnce(() -> commandState.setValue("Prep")), - launcher.trackTargetCommand(() -> getTargetPose())); - } - - private Command readyStateCommand() { - return Commands.parallel( - Commands.runOnce(() -> commandState.setValue("Ready")), - launcher.trackTargetCommand(() -> getTargetPose())); - } -} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleIO.java b/src/main/java/frc/robot/example/subsystems/ExampleIO.java deleted file mode 100644 index 89a53ee0..00000000 --- a/src/main/java/frc/robot/example/subsystems/ExampleIO.java +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example.subsystems; - -import static edu.wpi.first.units.Units.Degrees; - -import edu.wpi.first.units.measure.Angle; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import org.littletonrobotics.junction.AutoLog; - -/** Add your docs here. */ -public interface ExampleIO { - @AutoLog - public static class ExampleIOInputs { - public double shooterSpeedDesired = 0.0; - public double elevatorHeightDesired = 0.0; - public Angle hoodAngleDesired = Degrees.of(0.0); - public Angle turretAngleDesired = Degrees.of(0.0); - - public double shooterSpeedActual = 0.0; - public double elevatorHeightActual = 0.0; - public Angle hoodAngleActual = Degrees.of(0.0); - public Angle turretAngleActual = Degrees.of(0.0); - - public boolean shooterSpeedAtGoal = false; - public boolean elevatorHeightAtGoal = false; - public boolean hoodAngleAtGoal = false; - public boolean turretAngleAtGoal = false; - - public double shooterSpeedError = 0.0; - public double elevatorHeightError = 0.0; - public double hoodAngleError = 0.0; - public double turretAngleError = 0.0; - - public double hoodVelocity = 0.0; - public double turretVelocity = 0.0; - public double elevatorVelocity = 0.0; - public double shooterMotorOutput = 0.0; - public double elevatorMotorOutput = 0.0; - } - - public default void updateInputs(ExampleIOInputs inputs) {} - - public default void updateSimulation() {} - - public void setPercentMotor(double output); - - public Command setDutyCycle(double output); - - public void runShooter(double speed); - - public Command setUpperSpeed(AngularVelocity speed); - - public Command setElevatorHeight(double height); - - public void setHoodAngle(Angle angle); - - public void setTurretRotation(Angle angle); - - public AngularVelocity getShooterVelocity(); - - public Command sysIdShooter(); - - public Command sysIdArm(); - - public Command sysIdPivot(); - - public Command sysIdTurret(); - - public default Command addBallToRobot() { - return Commands.none(); - } - - public Command launchBall(); -} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleIOReal.java b/src/main/java/frc/robot/example/subsystems/ExampleIOReal.java deleted file mode 100644 index 6bfd0a00..00000000 --- a/src/main/java/frc/robot/example/subsystems/ExampleIOReal.java +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example.subsystems; - -import static edu.wpi.first.units.Units.Seconds; -import static edu.wpi.first.units.Units.Volts; - -import edu.wpi.first.units.measure.Angle; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import java.util.Map; -import org.frc5010.common.arch.GenericSubsystem; -import org.frc5010.common.motors.SystemIdentification; -import org.frc5010.common.motors.function.AngularControlMotor; -import org.frc5010.common.motors.function.PercentControlMotor; -import org.frc5010.common.motors.function.VelocityControlMotor; -import yams.mechanisms.positional.Arm; -import yams.mechanisms.positional.Pivot; -import yams.mechanisms.velocity.FlyWheel; - -/** Add your docs here. */ -public class ExampleIOReal implements ExampleIO { - protected Map devices; - protected PercentControlMotor percentMotor; - protected VelocityControlMotor controlledMotor; - protected AngularControlMotor angularMotor; - protected FlyWheel shooter; - protected Arm arm; - protected GenericSubsystem parent; - protected Pivot pivot; - - public ExampleIOReal(Map devices, GenericSubsystem parent) { - this.devices = devices; - this.parent = parent; - this.percentMotor = (PercentControlMotor) devices.get("percent_motor"); - this.controlledMotor = (VelocityControlMotor) devices.get("velocity_motor"); - this.shooter = (FlyWheel) devices.get("Shooter"); - this.arm = (Arm) devices.get("Hood"); - this.pivot = (Pivot) devices.get("Turret"); - this.angularMotor = (AngularControlMotor) devices.get("angular_motor"); - } - - @Override - public void updateInputs(ExampleIOInputs inputs) { - - angularMotor.periodicUpdate(); - } - - @Override - public void setPercentMotor(double output) { - percentMotor.set(output); - } - - @Override - public void runShooter(double speed) { - shooter.getMotor().setDutyCycle(speed); - } - - @Override - public Command setUpperSpeed(AngularVelocity speed) { - return shooter.setSpeed(speed); - } - - @Override - public Command setElevatorHeight(double height) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setElevatorHeight'"); - } - - @Override - public void setHoodAngle(Angle angle) { - arm.getMotorController().setPosition(angle); - } - - @Override - public void setTurretRotation(Angle angle) {} - - public AngularVelocity getShooterVelocity() { - return shooter.getSpeed(); - } - - public Command sysIdShooter() { - return SystemIdentification.getSysIdFullCommand( - SystemIdentification.rpmSysIdRoutine(shooter.getMotor(), parent.getName(), parent), - 5, - 3, - 3); - } - - public Command sysIdTurret() { - return SystemIdentification.getSysIdFullCommand( - SystemIdentification.angleSysIdRoutine( - pivot.getMotorController(), parent.getName(), parent), - 5, - 3, - 3); - } - - public Command sysIdArm() { - return arm.sysId(Volts.of(12), Volts.of(1).per(Seconds), Seconds.of(10)); - } - - public Command sysIdPivot() { - return pivot.sysId(Volts.of(12), Volts.of(1).per(Seconds), Seconds.of(10)); - } - - public Command launchBall() { - return shooter.set(0); - } - - public Command setDutyCycle(double output) { - return Commands.runOnce(() -> percentMotor.set(output), parent); - } -} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleIOSim.java b/src/main/java/frc/robot/example/subsystems/ExampleIOSim.java deleted file mode 100644 index 3527c0a1..00000000 --- a/src/main/java/frc/robot/example/subsystems/ExampleIOSim.java +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -package frc.robot.example.subsystems; - -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.Meters; -import static edu.wpi.first.units.Units.MetersPerSecond; - -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; -import java.util.Map; -import org.frc5010.common.arch.GenericSubsystem; -import org.frc5010.common.drive.GenericDrivetrain; -import org.frc5010.common.drive.swerve.YAGSLSwerveDrivetrain; -import org.frc5010.lobbinloco.FRC5010BallOnTheFly; -import org.littletonrobotics.junction.Logger; -import swervelib.simulation.ironmaple.simulation.IntakeSimulation; -import swervelib.simulation.ironmaple.simulation.IntakeSimulation.IntakeSide; -import swervelib.simulation.ironmaple.simulation.SimulatedArena; -import swervelib.simulation.ironmaple.simulation.gamepieces.GamePieceProjectile; -import swervelib.simulation.ironmaple.simulation.seasonspecific.crescendo2024.NoteOnFly; -import swervelib.simulation.ironmaple.simulation.seasonspecific.rebuilt2026.RebuiltFuelOnFly; - -/** Add your docs here. */ -public class ExampleIOSim extends ExampleIOReal { - protected IntakeSimulation intakeSimulation; - protected NoteOnFly noteOnFly; - protected RebuiltFuelOnFly fuelOnFly; - protected GamePieceProjectile gamePieceProjectile; - - public ExampleIOSim(Map devices, GenericSubsystem parent) { - super(devices, parent); - intakeSimulation = - IntakeSimulation.InTheFrameIntake( - "FRC5010Ball", - GenericDrivetrain.getMapleSimDrive().get(), - Inches.of(24.25), - IntakeSide.FRONT, - 1); - } - - @Override - public void updateSimulation() { - angularMotor.simulationUpdate(); - } - - @Override - public void setPercentMotor(double speed) { - if (speed > 0.0 && !noteIsInsideIntake().getAsBoolean()) { - intakeSimulation.startIntake(); - } else { - intakeSimulation.stopIntake(); - } - super.setPercentMotor(speed); - } - - public Trigger obtainedGamePieceToScore() { - return new Trigger( - () -> { - return RobotBase.isSimulation() - ? intakeSimulation.getGamePiecesAmount() == 1 - && intakeSimulation.obtainGamePieceFromIntake() - : false; - }); - } - - public Trigger noteIsInsideIntake() { - return new Trigger( - () -> { - return RobotBase.isSimulation() ? intakeSimulation.getGamePiecesAmount() > 0 : false; - }); - } - - public Command addBallToRobot() { - return Commands.runOnce(() -> intakeSimulation.addGamePieceToIntake()); - } - - @Override - public Command launchBall() { - return super.launchBall() - .alongWith( - Commands.runOnce( - () -> { - if (RobotBase.isSimulation()) { - Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); - gamePieceProjectile = - new FRC5010BallOnTheFly( - worldPose.getTranslation(), - controlledMotor - .getRobotToMotor() - .getTranslation() - .toTranslation2d(), - YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), - worldPose.getRotation(), - Meters.of(0.45), - MetersPerSecond.of(10), - Degrees.of(55)) - .withProjectileTrajectoryDisplayCallBack( - (pose3ds) -> { - Logger.recordOutput( - parent.getName() + "/GPTrajectory", - pose3ds.toArray(Pose3d[]::new)); - }); - SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); - } - })); - } -} diff --git a/src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java b/src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java deleted file mode 100644 index b8398c82..00000000 --- a/src/main/java/frc/robot/example/subsystems/ExampleSubsystem.java +++ /dev/null @@ -1,179 +0,0 @@ -package frc.robot.example.subsystems; - -import static edu.wpi.first.units.Units.Degrees; -import static edu.wpi.first.units.Units.Inches; - -import com.revrobotics.spark.SparkMax; -import edu.wpi.first.math.geometry.Pose3d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.util.Units; -import edu.wpi.first.wpilibj.RobotBase; -import edu.wpi.first.wpilibj2.command.Command; -import edu.wpi.first.wpilibj2.command.Commands; -import frc.robot.example.subsystems.ExampleIO.ExampleIOInputs; -import java.util.function.Supplier; -import org.frc5010.common.arch.GenericSubsystem; -import org.frc5010.common.constants.GenericPID; -import org.frc5010.common.constants.MotorFeedFwdConstants; -import org.frc5010.common.motors.MotorConstants.Motor; -import org.frc5010.common.motors.MotorFactory; -import org.frc5010.common.motors.function.AngularControlMotor; -import org.frc5010.common.sensors.absolute_encoder.RevAbsoluteEncoder; - -public class ExampleSubsystem extends GenericSubsystem { - protected ExampleIO io; - protected ExampleIOInputs inputs = new ExampleIOInputs(); - protected int scoredNotes = 0; - protected Rotation2d rotation = new Rotation2d(Degrees.of(180)); - - public ExampleSubsystem() { - super("example.json"); - devices.put("angular_motor", angularControlledMotor()); - if (RobotBase.isSimulation()) { - io = new ExampleIOSim(devices, this); - } else { - io = new ExampleIOReal(devices, this); - } - } - - public Command trackTargetCommand(Supplier targetSupplier) { - return Commands.run( - () -> { - Translation2d targetPose = targetSupplier.get(); - targetPose.getNorm(); - io.setTurretRotation(targetPose.getAngle().getMeasure()); - io.setHoodAngle(Degrees.of(45)); - }); - } - - public Command stopTrackingCommand() { - return Commands.runOnce( - () -> { - io.setTurretRotation(Degrees.of(0)); - io.setHoodAngle(Degrees.of(0)); - }); - } - - public Command intakeCommand() { - return Commands.runOnce(() -> io.setPercentMotor(0.5), this); - } - - public Command stopIntakeCommand() { - return Commands.runOnce(() -> io.setPercentMotor(0.0), this); - } - - public Command sysIdShooter() { - return io.sysIdShooter(); - } - - public Command addBallToRobot() { - return io.addBallToRobot(); - } - - public Command launchBall() { - return io.launchBall(); - } - - public Command sysIdArm() { - return io.sysIdArm(); - } - - public Command sysIdPivot() { - return io.sysIdPivot(); - } - - public Command sysIdTurret() { - return io.sysIdTurret(); - } - - @Override - public void periodic() { - super.periodic(); - io.updateInputs(inputs); - } - - @Override - public void simulationPeriodic() { - super.simulationPeriodic(); - io.updateSimulation(); - } - - public AngularControlMotor angularControlledMotor() { - AngularControlMotor angularMotor = - new AngularControlMotor( - MotorFactory.Spark(13, Motor.Neo), "angular", getDisplayValuesHelper()) - .setupSimulatedMotor( - (5.0 * 68.0 / 24.0) * (80.0 / 24.0), - Units.lbsToKilograms(22), - Inches.of(19), - Degrees.of(0), - Degrees.of(360), - false, - 0, - Degrees.of(0), - false, - 0.1) - .setVisualizer(mechanismSimulation, new Pose3d(0.75, 0, 0.25, new Rotation3d())); - angularMotor.setEncoder(new RevAbsoluteEncoder((SparkMax) angularMotor.getMotor(), 360)); - angularMotor.setValues(new GenericPID(0.01, 0.000025, 0.003)); - angularMotor.setMotorFeedFwd(new MotorFeedFwdConstants(0.0, 0.01, 0.0, false)); - angularMotor.setIZone(3); - angularMotor.setOutputRange(-12, 12); - return angularMotor; - } - - // public Command setVelocityControlMotorReference(DoubleSupplier reference) { - // return Commands.runOnce( - // () -> { - // double speed = reference.getAsDouble(); - // if (speed <= 0.0 && !noteIsInsideIntake().getAsBoolean()) { - // controlledMotor.setReference(speed); - // } else if (speed > 3000 - // && noteIsInsideIntake().getAsBoolean() - // && obtainedGamePieceToScore().getAsBoolean()) { - // controlledMotor.setReference(speed); - // if (RobotBase.isSimulation()) { - // Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); - // gamePieceProjectile = - // new ReefscapeAlgaeOnFly( - // worldPose.getTranslation(), - // controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), - // YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), - // worldPose.getRotation(), - // Meters.of(0.45), - // MetersPerSecond.of(speed / 6000 * 20), - // Degrees.of(55)); - // SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); - // } - // } else if (speed < 3000 - // && speed > 1000 - // && noteIsInsideIntake().getAsBoolean() - // && obtainedGamePieceToScore().getAsBoolean()) { - // controlledMotor.setReference(speed); - // if (RobotBase.isSimulation()) { - // Pose2d worldPose = YAGSLSwerveDrivetrain.getSwerveDrive().getPose(); - // gamePieceProjectile = - // new ReefscapeAlgaeOnFly( - // worldPose.getTranslation(), - // controlledMotor.getRobotToMotor().getTranslation().toTranslation2d(), - // YAGSLSwerveDrivetrain.getSwerveDrive().getFieldVelocity(), - // worldPose.getRotation(), - // Meters.of(0.45), - // MetersPerSecond.of(speed / 6000 * 20), - // Degrees.of(55)); - // SimulatedArena.getInstance().addGamePieceProjectile(gamePieceProjectile); - // } - // } else { - // controlledMotor.setReference(speed); - // } - // }, - // this); - // } - - // public Command setAngularMotorReference(DoubleSupplier reference) { - // return Commands.runOnce(() -> angularMotor.setReference(reference.getAsDouble()), this); - // } - -} From dbd87d0318efc048fa32fcd1a108c48584010664 Mon Sep 17 00:00:00 2001 From: Curt Date: Sat, 4 Apr 2026 12:41:00 -0400 Subject: [PATCH 54/93] Update flywheel configs and robot simulate flags Normalize device entries to an array for upper/lower flywheels, update motor tuning (feedForward and feedback P) and simulation feedforward values, rename simMotorSystemId->simSystemId, rename stages->gearStages and diameter->radius (radius val 2 in) in both flywheel configs. Also flip simulation flags in robots.json: set black_robot simulate=false and buttercup_swerve simulate=true. Affects lowerflywheel.json, lowerflywheel/flywheel.json, upperflywheel.json, upperflywheel/flywheel.json and robots.json. --- .../subsystems/lowerflywheel.json | 9 +++++--- .../subsystems/lowerflywheel/flywheel.json | 22 +++++++++---------- .../subsystems/upperflywheel.json | 9 +++++--- .../subsystems/upperflywheel/flywheel.json | 16 +++++++------- src/main/deploy/robots.json | 4 ++-- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json index 7ce8b5e6..229cec66 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel.json @@ -1,5 +1,8 @@ { - "devices": { - "yams_shooter": "lowerflywheel/flywheel.json" - } + "devices": [ + { + "device": "yams_shooter", + "file": "lowerflywheel/flywheel.json" + } + ] } diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json index aac808c3..67975bc1 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json @@ -8,26 +8,26 @@ }, "motorSystemId": { "feedBack": { - "p": 0, + "p": 3.0352E-05, "i": 0.0, "d": 0.0 }, "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 + "s": 0.04234, + "v": 0.74358, + "a": 0.098446 } }, - "simMotorSystemId": { + "simSystemId": { "feedBack": { "p": 0, "i": 0.0, "d": 0.0 }, "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 + "s": 0.04234, + "v": 0.11449, + "a": 0.094235 } }, "lowerSoftLimit": { @@ -38,13 +38,13 @@ "val": 10000, "uom": "rpm" }, - "stages": "52:24", + "gearStages": "52:24", "mass": { "val": 2, "uom": "kg" }, - "diameter": { - "val": 4, + "radius": { + "val": 2, "uom": "in" } } diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json index 9056d051..41742573 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel.json @@ -1,5 +1,8 @@ { - "devices": { - "yams_shooter": "upperflywheel/flywheel.json" - } + "devices": [ + { + "device": "yams_shooter", + "file": "upperflywheel/flywheel.json" + } + ] } diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json index 9966d1c7..0d5f0a22 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json @@ -8,17 +8,17 @@ }, "motorSystemId": { "feedBack": { - "p": 0, + "p": 3.0352E-05, "i": 0.0, "d": 0.0 }, "feedForward": { - "s": 0.0, - "v": 0.0, - "a": 0.0 + "s": 0.04234, + "v": 0.74358, + "a": 0.098446 } }, - "simMotorSystemId": { + "simSystemId": { "feedBack": { "p": 0, "i": 0.0, @@ -38,13 +38,13 @@ "val": 10000, "uom": "rpm" }, - "stages": "52:24", + "gearStages": "52:24", "mass": { "val": 2, "uom": "kg" }, - "diameter": { - "val": 4, + "radius": { + "val": 2, "uom": "in" } } diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index ad1396ac..1448652f 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -4,13 +4,13 @@ "black_robot": { "id": "00:80:2F:33:04:33", "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": true, + "simulate": false, "competition": false }, "buttercup_swerve": { "id": "00:80:2F:34:B2:C5", "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": false, + "simulate": true, "competition": false } } From 4168e2326219a273ee01f2a5999f24bae01cb162 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Sat, 4 Apr 2026 14:02:21 -0400 Subject: [PATCH 55/93] Removing LobbinLoco --- src/main/deploy/buttercup_swerve/cameras.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/deploy/buttercup_swerve/cameras.json b/src/main/deploy/buttercup_swerve/cameras.json index f308fd3e..10e9384c 100644 --- a/src/main/deploy/buttercup_swerve/cameras.json +++ b/src/main/deploy/buttercup_swerve/cameras.json @@ -1,5 +1,4 @@ { "cameras": [], - "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" } From 72dc996c5121fe0eee9b0247f60dc647c1eafe11 Mon Sep 17 00:00:00 2001 From: Curt Date: Sat, 4 Apr 2026 15:35:12 -0400 Subject: [PATCH 56/93] Configure drivetrain buttons; remove aprilTagLayout Wire up drivetrain controls in BlackRobot by calling drivetrain.configureButtonBindings and binding the right stick button to toggle field-oriented driving. Also remove the unused "aprilTagLayout" entry from cameras.json to avoid referencing a non-existent layout path. --- src/main/deploy/black_robot/cameras.json | 1 - src/main/java/frc/robot/blackteam/BlackRobot.java | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/deploy/black_robot/cameras.json b/src/main/deploy/black_robot/cameras.json index f308fd3e..10e9384c 100644 --- a/src/main/deploy/black_robot/cameras.json +++ b/src/main/deploy/black_robot/cameras.json @@ -1,5 +1,4 @@ { "cameras": [], - "aprilTagLayout": "/org/frc5010/lobbinloco/LobbinLoco.json", "simulatedField": "org.frc5010.lobbinloco.LobbinLoco" } diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index a446a827..8e0f9de3 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -50,6 +50,10 @@ private Command launchToDistance(int i) { @Override public void configureButtonBindings(Controller driver, Controller operator) { + drivetrain.configureButtonBindings(driver, operator); + driver + .createRightStickButton() + .onTrue(Commands.runOnce(() -> drivetrain.toggleFieldOrientedDrive())); driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); LEDStrip.setSegmentActive(FEEDER_LED, false); From b78c8e0f169c4458dd5465155b8f971f8ff629c7 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Wed, 8 Apr 2026 18:11:00 -0400 Subject: [PATCH 57/93] Fixed inversions and stuff --- .../subsystems/lowerflywheel/flywheel.json | 3 ++- .../subsystems/upperflywheel/flywheel.json | 3 ++- .../axis/driver_right_trigger.json | 4 ++-- .../subsystems/lowerflywheel/flywheel.json | 2 +- .../subsystems/upperflywheel/flywheel.json | 2 +- .../java/frc/robot/blackteam/BlackRobot.java | 24 +++++++++---------- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json index e1b0df38..8c89be2d 100644 --- a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json @@ -3,7 +3,8 @@ "name": "lowerflywheel", "controllerType": "thrifty", "motorType": "Neo", - "canId": 10 + "canId": 10, + "inverted": false }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json index dd3663bd..f63ab13d 100644 --- a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json @@ -3,7 +3,8 @@ "name": "upperflywheel", "controllerType": "thrifty", "motorType": "Neo", - "canId": 11 + "canId": 11, + "inverted": false }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json index c84d2264..bd2ec447 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_trigger.json @@ -2,7 +2,7 @@ "channel": 3, "deadband": 0.07, "invert": false, - "scale": 0.5, + "scale": 1.0, "curvePower": 3, - "limit": 0.5 + "limit": 1.0 } diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json index 67975bc1..8c89be2d 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json @@ -4,7 +4,7 @@ "controllerType": "thrifty", "motorType": "Neo", "canId": 10, - "inverted": true + "inverted": false }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json index 0d5f0a22..f63ab13d 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json @@ -4,7 +4,7 @@ "controllerType": "thrifty", "motorType": "Neo", "canId": 11, - "inverted": true + "inverted": false }, "motorSystemId": { "feedBack": { diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 8e0f9de3..7f65aff5 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -22,10 +22,10 @@ public class BlackRobot extends GenericRobot { private LowerFlyWheel lowerFlyWheel; private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); - private final double SPEED1 = 0.15; - private final double SPEED2 = 0.35; - private final double SPEED3 = 0.16; - private final double SPEED4 = 0.17; + private final double SPEED1 = -0.15; + private final double SPEED2 = -0.35; + private final double SPEED3 = -0.16; + private final double SPEED4 = -0.17; private final double UPPEROFFSET = 0.01; private final AngularVelocity VELOCITY2 = RPM.of(1500); private final String FEEDER_LED = "left_half"; @@ -64,7 +64,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { .whileTrue( lowerFlyWheel .set(SPEED1) - .alongWith(upperFlyWheel.set(SPEED1 + UPPEROFFSET)) + .alongWith(upperFlyWheel.set(SPEED1 - UPPEROFFSET)) .beforeStarting( () -> { LEDStrip.setSegmentActive(SHOOTER_LED, true); @@ -77,7 +77,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { .whileTrue( lowerFlyWheel .set(SPEED2) - .alongWith(upperFlyWheel.set(SPEED2 + UPPEROFFSET)) + .alongWith(upperFlyWheel.set(SPEED2 - UPPEROFFSET)) .beforeStarting( () -> { LEDStrip.setSegmentActive(SHOOTER_LED, true); @@ -90,7 +90,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { .whileTrue( lowerFlyWheel .set(SPEED3) - .alongWith(upperFlyWheel.set(SPEED3 + UPPEROFFSET)) + .alongWith(upperFlyWheel.set(SPEED3 - UPPEROFFSET)) .beforeStarting( () -> { LEDStrip.setSegmentActive(SHOOTER_LED, true); @@ -102,7 +102,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { .whileTrue( lowerFlyWheel .set(SPEED4) - .alongWith(upperFlyWheel.set(SPEED4 + UPPEROFFSET)) + .alongWith(upperFlyWheel.set(SPEED4 - UPPEROFFSET)) .beforeStarting( () -> { LEDStrip.setSegmentActive(SHOOTER_LED, true); @@ -129,8 +129,8 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - driver.LEFT_BUMPER.onFalse( - (feeder.setSpeed(0)).andThen(lowerFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + // driver.LEFT_BUMPER.onFalse( + // (feeder.setSpeed(0)).andThen(lowerFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); driver .createRightBumper() @@ -149,8 +149,8 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - driver.RIGHT_BUMPER.onFalse( - (feeder.setSpeed(0)).andThen(upperFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + // driver.RIGHT_BUMPER.onFalse( + // (feeder.setSpeed(0)).andThen(upperFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); State prep = flyWheelStateMachine.addState( From 8748a97ffc59d88be5af96f2815fed9bbf04f336 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Sat, 11 Apr 2026 15:37:20 -0400 Subject: [PATCH 58/93] Both robots work --- .../subsystems/upperflywheel/flywheel.json | 2 +- .../subsystems/lowerflywheel/flywheel.json | 4 ++-- .../subsystems/upperflywheel/flywheel.json | 4 ++-- src/main/deploy/robots.json | 12 ++++++------ src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json index f63ab13d..0d5f0a22 100644 --- a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json @@ -4,7 +4,7 @@ "controllerType": "thrifty", "motorType": "Neo", "canId": 11, - "inverted": false + "inverted": true }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json index 8c89be2d..694665db 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/lowerflywheel/flywheel.json @@ -1,10 +1,10 @@ { "motorSetup": { "name": "lowerflywheel", - "controllerType": "thrifty", + "controllerType": "Spark", "motorType": "Neo", "canId": 10, - "inverted": false + "inverted": true }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json index f63ab13d..8fdf840a 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/upperflywheel/flywheel.json @@ -1,10 +1,10 @@ { "motorSetup": { "name": "upperflywheel", - "controllerType": "thrifty", + "controllerType": "Spark", "motorType": "Neo", "canId": 11, - "inverted": false + "inverted": true }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/robots.json b/src/main/deploy/robots.json index 1448652f..4b5ca5ad 100644 --- a/src/main/deploy/robots.json +++ b/src/main/deploy/robots.json @@ -1,17 +1,17 @@ { "competitionPin": 0, "robots": { - "black_robot": { - "id": "00:80:2F:33:04:33", - "robotClass": "frc.robot.blackteam.BlackRobot", - "simulate": false, - "competition": false - }, "buttercup_swerve": { "id": "00:80:2F:34:B2:C5", "robotClass": "frc.robot.blackteam.BlackRobot", "simulate": true, "competition": false + }, + "black_robot": { + "id": "00:80:2F:33:04:33", + "robotClass": "frc.robot.blackteam.BlackRobot", + "simulate": false, + "competition": false } } } diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 137dba76..6d2de636 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 115; - public static final String GIT_SHA = "85619b0eac2a4edd08a9260245cfbbdde0857a4c"; - public static final String GIT_DATE = "2026-03-30 18:20:56 EDT"; + public static final int GIT_REVISION = 182; + public static final String GIT_SHA = "b78c8e0f169c4458dd5465155b8f971f8ff629c7"; + public static final String GIT_DATE = "2026-04-08 18:11:00 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2026-04-01 12:58:06 EDT"; - public static final long BUILD_UNIX_TIME = 1775062686859L; + public static final String BUILD_DATE = "2026-04-11 15:34:36 EDT"; + public static final long BUILD_UNIX_TIME = 1775936076916L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 7f65aff5..d7dae7bb 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -22,10 +22,10 @@ public class BlackRobot extends GenericRobot { private LowerFlyWheel lowerFlyWheel; private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); - private final double SPEED1 = -0.15; - private final double SPEED2 = -0.35; - private final double SPEED3 = -0.16; - private final double SPEED4 = -0.17; + private final double SPEED1 = 0.15; + private final double SPEED2 = 0.35; + private final double SPEED3 = 0.16; + private final double SPEED4 = 0.17; private final double UPPEROFFSET = 0.01; private final AngularVelocity VELOCITY2 = RPM.of(1500); private final String FEEDER_LED = "left_half"; From 1224266a9ac76f5f7052e92a611aaa281410df3c Mon Sep 17 00:00:00 2001 From: 5010 FHS 6 Date: Wed, 15 Apr 2026 18:36:45 -0400 Subject: [PATCH 59/93] Switch feeder to SparkMax; Fixed Feeder from Continuously running. Replaced ThriftyNova/NovaWrapper with revai SparkMax and SparkWrapper in FeederSubsystem (imported MotorType/SparkMax, instantiated SparkMax with kBrushless, and updated wrapper). Un-commented LEFT_BUMPER and RIGHT_BUMPER onFalse handlers in BlackRobot to stop feeder and flywheels. Updated BuildConstants (GIT_REVISION, GIT_SHA, GIT_DATE, BUILD_DATE, BUILD_UNIX_TIME). Minor trailing comments added to BlackRobot and FeederSubsystem. --- src/main/java/frc/robot/BuildConstants.java | 10 +++++----- src/main/java/frc/robot/blackteam/BlackRobot.java | 12 ++++++++---- .../java/frc/robot/blackteam/FeederSubsystem.java | 11 ++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 6d2de636..48dca6b8 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 182; - public static final String GIT_SHA = "b78c8e0f169c4458dd5465155b8f971f8ff629c7"; - public static final String GIT_DATE = "2026-04-08 18:11:00 EDT"; + public static final int GIT_REVISION = 183; + public static final String GIT_SHA = "8748a97ffc59d88be5af96f2815fed9bbf04f336"; + public static final String GIT_DATE = "2026-04-11 15:37:20 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2026-04-11 15:34:36 EDT"; - public static final long BUILD_UNIX_TIME = 1775936076916L; + public static final String BUILD_DATE = "2026-04-15 16:57:36 EDT"; + public static final long BUILD_UNIX_TIME = 1776286656632L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index d7dae7bb..fffc0d5b 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -129,8 +129,8 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - // driver.LEFT_BUMPER.onFalse( - // (feeder.setSpeed(0)).andThen(lowerFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + driver.LEFT_BUMPER.onFalse( + (feeder.setSpeed(0)).andThen(lowerFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); driver .createRightBumper() @@ -149,8 +149,8 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - // driver.RIGHT_BUMPER.onFalse( - // (feeder.setSpeed(0)).andThen(upperFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + driver.RIGHT_BUMPER.onFalse( + (feeder.setSpeed(0)).andThen(upperFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); State prep = flyWheelStateMachine.addState( @@ -197,3 +197,7 @@ public void buildAutoCommands() { selectableCommand.addOption("Do Nothing", Commands.none()); } } +// frontright has CanId 7 and opposite side of RSL and by the battery +// +// +// \ No newline at end of file diff --git a/src/main/java/frc/robot/blackteam/FeederSubsystem.java b/src/main/java/frc/robot/blackteam/FeederSubsystem.java index 31860130..03337369 100644 --- a/src/main/java/frc/robot/blackteam/FeederSubsystem.java +++ b/src/main/java/frc/robot/blackteam/FeederSubsystem.java @@ -11,7 +11,8 @@ import static edu.wpi.first.units.Units.RotationsPerSecondPerSecond; import static edu.wpi.first.units.Units.Seconds; -import com.thethriftybot.devices.ThriftyNova; +import com.revrobotics.spark.SparkLowLevel.MotorType; +import com.revrobotics.spark.SparkMax; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.wpilibj2.command.Command; import java.util.function.Supplier; @@ -25,12 +26,11 @@ import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; -import yams.motorcontrollers.local.NovaWrapper; +import yams.motorcontrollers.local.SparkWrapper; /** Add your docs here. */ public class FeederSubsystem extends GenericSubsystem { - private final ThriftyNova motor = new ThriftyNova(12); - + private final SparkMax motor = new SparkMax(12, MotorType.kBrushless); private final SmartMotorControllerConfig motorConfig = new SmartMotorControllerConfig(this) .withClosedLoopController( @@ -46,7 +46,7 @@ public class FeederSubsystem extends GenericSubsystem { .withControlMode(ControlMode.CLOSED_LOOP); private final SmartMotorController motorController = - new NovaWrapper(motor, DCMotor.getNEO(1), motorConfig); + new SparkWrapper(motor, DCMotor.getNEO(1), motorConfig); private final FlyWheelConfig feederConfig = new FlyWheelConfig(motorController) @@ -77,3 +77,4 @@ public Command joyStickControl(Supplier speedSupplier) { return feeder.set(speedSupplier); } } +/** Robot, stop */ From 99e2684e6b3a48f63354969618208049de8f9f2a Mon Sep 17 00:00:00 2001 From: 5010 FHS 6 Date: Wed, 22 Apr 2026 19:00:09 -0400 Subject: [PATCH 60/93] Demo mode --- src/main/deploy/black_robot/demo_mode.json | 4 ++-- src/main/deploy/black_robot/robot.json | 2 +- src/main/deploy/buttercup_swerve/demo_mode.json | 4 ++-- src/main/deploy/buttercup_swerve/robot.json | 2 +- .../yagsl_swerve/modules/pidfproperties.json | 2 +- src/main/java/frc/robot/blackteam/BlackRobot.java | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/deploy/black_robot/demo_mode.json b/src/main/deploy/black_robot/demo_mode.json index f60f73c9..277fdefa 100644 --- a/src/main/deploy/black_robot/demo_mode.json +++ b/src/main/deploy/black_robot/demo_mode.json @@ -1,6 +1,6 @@ { - "maxSpeed": 2.0, - "maxAngularSpeed": 2.0, + "maxSpeed": 0.25, + "maxAngularSpeed": 1.0, "maxAccelleration": 1.0, "maxAngularAccelleration": 9.0 } diff --git a/src/main/deploy/black_robot/robot.json b/src/main/deploy/black_robot/robot.json index 8c34455f..086c07a3 100644 --- a/src/main/deploy/black_robot/robot.json +++ b/src/main/deploy/black_robot/robot.json @@ -1,5 +1,5 @@ { - "userConfig": "competition_mode.json", + "userConfig": "demo_mode.json", "driveType": "YAGSL_SWERVE_DRIVE", "trackWidth": 18.5625, "trackWidthUom": "in", diff --git a/src/main/deploy/buttercup_swerve/demo_mode.json b/src/main/deploy/buttercup_swerve/demo_mode.json index f60f73c9..277fdefa 100644 --- a/src/main/deploy/buttercup_swerve/demo_mode.json +++ b/src/main/deploy/buttercup_swerve/demo_mode.json @@ -1,6 +1,6 @@ { - "maxSpeed": 2.0, - "maxAngularSpeed": 2.0, + "maxSpeed": 0.25, + "maxAngularSpeed": 1.0, "maxAccelleration": 1.0, "maxAngularAccelleration": 9.0 } diff --git a/src/main/deploy/buttercup_swerve/robot.json b/src/main/deploy/buttercup_swerve/robot.json index a2abafde..75cbef10 100644 --- a/src/main/deploy/buttercup_swerve/robot.json +++ b/src/main/deploy/buttercup_swerve/robot.json @@ -1,5 +1,5 @@ { - "userConfig": "competition_mode.json", + "userConfig": "demo_mode.json", "driveType": "YAGSL_SWERVE_DRIVE", "trackWidth": 18.5625, "trackWidthUom": "in", diff --git a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json index 7fd0a90a..a5136120 100644 --- a/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/buttercup_swerve/yagsl_swerve/modules/pidfproperties.json @@ -7,7 +7,7 @@ "iz": 0 }, "angle": { - "p": 0.01, + "p": 0.02, "i": 0, "d": 0.0000, "f": 0, diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index fffc0d5b..f27cf711 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -197,7 +197,7 @@ public void buildAutoCommands() { selectableCommand.addOption("Do Nothing", Commands.none()); } } -// frontright has CanId 7 and opposite side of RSL and by the battery -// -// -// \ No newline at end of file +// frontright has CanId 1, 2 and opposite side of RSL and by the battery +// frontleft has CanId 3, 4 and next to the RS light +// backright has CanId 7, 8 and next to the feeder motor with CanId 12 +// backleft has CanId 5, 6 and next to lower flywheel motor with CanId 10 From 20f3739ab85cae952d1b5c5fec9abd11e627bf13 Mon Sep 17 00:00:00 2001 From: 5010 FHS 6 Date: Fri, 24 Apr 2026 17:50:26 -0400 Subject: [PATCH 61/93] Fix Black launcher and fix interference with shooting --- src/main/deploy/black_robot/demo_mode.json | 2 +- .../black_robot/subsystems/lowerflywheel/flywheel.json | 2 +- .../black_robot/subsystems/upperflywheel/flywheel.json | 4 ++-- src/main/deploy/buttercup_swerve/demo_mode.json | 2 +- src/main/java/frc/robot/blackteam/BlackRobot.java | 6 ++---- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/deploy/black_robot/demo_mode.json b/src/main/deploy/black_robot/demo_mode.json index 277fdefa..865cc1b5 100644 --- a/src/main/deploy/black_robot/demo_mode.json +++ b/src/main/deploy/black_robot/demo_mode.json @@ -1,5 +1,5 @@ { - "maxSpeed": 0.25, + "maxSpeed": 0.40, "maxAngularSpeed": 1.0, "maxAccelleration": 1.0, "maxAngularAccelleration": 9.0 diff --git a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json index 8c89be2d..c26ad551 100644 --- a/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/lowerflywheel/flywheel.json @@ -1,7 +1,7 @@ { "motorSetup": { "name": "lowerflywheel", - "controllerType": "thrifty", + "controllerType": "Spark", "motorType": "Neo", "canId": 10, "inverted": false diff --git a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json index 0d5f0a22..618ac0a1 100644 --- a/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/upperflywheel/flywheel.json @@ -1,10 +1,10 @@ { "motorSetup": { "name": "upperflywheel", - "controllerType": "thrifty", + "controllerType": "Spark", "motorType": "Neo", "canId": 11, - "inverted": true + "inverted": false }, "motorSystemId": { "feedBack": { diff --git a/src/main/deploy/buttercup_swerve/demo_mode.json b/src/main/deploy/buttercup_swerve/demo_mode.json index 277fdefa..865cc1b5 100644 --- a/src/main/deploy/buttercup_swerve/demo_mode.json +++ b/src/main/deploy/buttercup_swerve/demo_mode.json @@ -1,5 +1,5 @@ { - "maxSpeed": 0.25, + "maxSpeed": 0.40, "maxAngularSpeed": 1.0, "maxAccelleration": 1.0, "maxAngularAccelleration": 9.0 diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index f27cf711..b6b939b2 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -129,8 +129,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - driver.LEFT_BUMPER.onFalse( - (feeder.setSpeed(0)).andThen(lowerFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + driver.LEFT_BUMPER.onFalse(feeder.setSpeed(0)); driver .createRightBumper() @@ -149,8 +148,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - driver.RIGHT_BUMPER.onFalse( - (feeder.setSpeed(0)).andThen(upperFlyWheel.set(0)).andThen(upperFlyWheel.set(0))); + driver.RIGHT_BUMPER.onFalse(feeder.setSpeed(0)); State prep = flyWheelStateMachine.addState( From aebb9337cd5757c80fb19d8e3a9cdb4cd7d90f8a Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 22 Jun 2026 17:47:54 -0400 Subject: [PATCH 62/93] switch the steer direction --- .../buttercup_swerve/controllers/axis/driver_right_x.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json index 8f981e23..007abd6c 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json @@ -1,7 +1,7 @@ { "channel": 4, "deadband": 0.07, - "invert": true, + "invert": false, "scale": 1.0, "curvePower": 3, "limit": 1.0 From a88e916ca3750a75efb494043a0d591972c79246 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 21:51:33 +0000 Subject: [PATCH 63/93] Configure buttercup AdvantageKit swerve from YAGSL values The buttercup akit_swerve_drivetrain.json previously contained only the drivetrain type, so the AKIT_SWERVE_DRIVE path would fail (null constants). Populate the full constants block, deriving every value from buttercup's existing YAGSL configuration: - Dimensions/speed from robot.json (track/wheelbase 18.5625 in, wheel 4 in, max speed 5.93 m/s) and robot mass 75 kg from physicalproperties.json. - Gear ratios 1:6.75 drive / 1:25 steer, wheelCOF 1.19, current limits (40 A drive / 20 A steer) and ramp rates from physicalproperties.json. - Pigeon2 gyro id 9 from swervedrive.json. - Drive/steer feedback P (0.0020645 / 0.02) from pidfproperties.json. - Per-module drive/steer CAN IDs, NEO/Spark motor setup, encoder inversion and absolute offsets (currently 0, pending calibration) from the YAGSL module JSONs. Validated against schemas/akit_swerve_drivetrain.schema.json. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../akit_swerve_drivetrain.json | 221 +++++++++++++++++- 1 file changed, 220 insertions(+), 1 deletion(-) diff --git a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json index 42f49455..2f2e9ac8 100644 --- a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json +++ b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json @@ -1,3 +1,222 @@ { - "type": "SparkTalon" + "type": "SparkTalon", + "constants": { + "trackWidth": { + "val": 18.5625, + "uom": "inches" + }, + "wheelBase": { + "val": 18.5625, + "uom": "inches" + }, + "wheelDiameter": { + "val": 4, + "uom": "inches" + }, + "maxDriveSpeed": { + "val": 5.93, + "uom": "m/sec" + }, + "bumperFrameLength": { + "val": 28, + "uom": "inches" + }, + "bumperFrameWidth": { + "val": 28, + "uom": "inches" + }, + "gyro": { + "type": "pigeon2", + "id": 9, + "inverted": false, + "canbus": "" + }, + "driveGearRatio": "1:6.75", + "steerGearRatio": "1:25", + "driveMotorControl": { + "closedLoopRamp": { + "val": 0.15, + "uom": "seconds" + }, + "feedBack": { + "p": 0.0020645, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "steerMotorControl": { + "closedLoopRamp": { + "val": 0.05, + "uom": "seconds" + }, + "feedBack": { + "p": 0.02, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "modules": { + "frontLeft": { + "driveMotorSetup": { + "name": "frontLeftDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 4, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "frontLeftSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 3, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + }, + "frontRight": { + "driveMotorSetup": { + "name": "frontRightDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 2, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "frontRightSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 1, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + }, + "backLeft": { + "driveMotorSetup": { + "name": "backLeftDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 6, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "backLeftSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 5, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + }, + "backRight": { + "driveMotorSetup": { + "name": "backRightDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 8, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "backRightSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 7, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + } + }, + "coupleRatio": 1.0, + "invertLeftSide": false, + "invertRightSide": false, + "steerInertia": { + "val": 0.04, + "uom": "kg*m^2" + }, + "driveInertia": { + "val": 0.025, + "uom": "kg*m^2" + }, + "robotMass": { + "val": 75, + "uom": "kg" + }, + "wheelCOF": 1.19, + "slipCurrent": { + "val": 40, + "uom": "amps" + }, + "canbus": "", + "startingPose": { + "x": { + "val": 1.0, + "uom": "m" + }, + "y": { + "val": 1.0, + "uom": "m" + }, + "rotation": { + "val": 0.0, + "uom": "deg" + } + } + } } From 3b6e84556ffe0c2ac2b1446adda0b492063a75a9 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 22 Jun 2026 17:57:00 -0400 Subject: [PATCH 64/93] Add Akit in --- src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json | 2 +- src/main/deploy/buttercup_swerve/robot.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json index 2f2e9ac8..355e07c5 100644 --- a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json +++ b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json @@ -1,5 +1,5 @@ { - "type": "SparkTalon", + "type": "Spark", "constants": { "trackWidth": { "val": 18.5625, diff --git a/src/main/deploy/buttercup_swerve/robot.json b/src/main/deploy/buttercup_swerve/robot.json index 75cbef10..5973e5f7 100644 --- a/src/main/deploy/buttercup_swerve/robot.json +++ b/src/main/deploy/buttercup_swerve/robot.json @@ -1,6 +1,6 @@ { "userConfig": "demo_mode.json", - "driveType": "YAGSL_SWERVE_DRIVE", + "driveType": "AKIT_SWERVE_DRIVE", "trackWidth": 18.5625, "trackWidthUom": "in", "wheelBase": 18.5625, From 5664b5beea6a28623bddbace5e83e9bd38476928 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 22:06:03 +0000 Subject: [PATCH 65/93] Make akit Spark path drive buttercup's hardware The "Spark" akit module IO reads hardware from DriveConstants.java, not the deploy JSON, so buttercup needed those constants to match its wiring for the Canandmag-via-Spark-absolute-encoder setup to actually drive the robot. DriveConstants.java (buttercup, from YAGSL config): - Drive/steer CAN IDs and Pigeon2 id (9). - Drive: NEO gearbox, 6.75:1 reduction, 4 in wheel, 40 A limit, kP 0.0020645. - Turn: NEO gearbox, 25:1; existing absolute-encoder turn config already reads the Canandmag through the Spark (kAbsoluteEncoder feedback, position wrapping, zeroRotation 0 relying on the Canandmag's internal zero). - Robot mass 75 kg, wheelCOF 1.19, max speed 5.93 m/s. ModuleIOSpark.java: - Construct the drive motor as a SparkMax (NEO) instead of SparkFlex (Vortex) to match buttercup's drive controllers. Drive velocity gain is carried from YAGSL but uses a different control-unit basis here, so it should be verified/tuned on the robot. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../drive/swerve/akit/DriveConstants.java | 53 ++++++++++--------- .../drive/swerve/akit/ModuleIOSpark.java | 6 +-- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/DriveConstants.java b/src/main/java/org/frc5010/common/drive/swerve/akit/DriveConstants.java index 580b4b19..74ffe626 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/DriveConstants.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/DriveConstants.java @@ -15,10 +15,11 @@ import edu.wpi.first.math.util.Units; public class DriveConstants { - public static final double maxSpeedMetersPerSec = 4.8; + // Buttercup values, derived from its YAGSL configuration (buttercup_swerve/yagsl_swerve) + public static final double maxSpeedMetersPerSec = 5.93; public static final double odometryFrequency = 100.0; // Hz - public static final double trackWidth = Units.inchesToMeters(26.5); - public static final double wheelBase = Units.inchesToMeters(26.5); + public static final double trackWidth = Units.inchesToMeters(18.5625); + public static final double wheelBase = Units.inchesToMeters(18.5625); public static final double driveBaseRadius = Math.hypot(trackWidth / 2.0, wheelBase / 2.0); public static final Translation2d[] moduleTranslations = new Translation2d[] { @@ -34,25 +35,24 @@ public class DriveConstants { public static final Rotation2d backLeftZeroRotation = new Rotation2d(0.0); public static final Rotation2d backRightZeroRotation = new Rotation2d(0.0); - // Device CAN IDs - public static final int pigeonCanId = 50; + // Device CAN IDs (buttercup) + public static final int pigeonCanId = 9; - public static final int frontLeftDriveCanId = 1; - public static final int backLeftDriveCanId = 3; - public static final int frontRightDriveCanId = 5; - public static final int backRightDriveCanId = 7; + public static final int frontLeftDriveCanId = 4; + public static final int backLeftDriveCanId = 6; + public static final int frontRightDriveCanId = 2; + public static final int backRightDriveCanId = 8; - public static final int frontLeftTurnCanId = 2; - public static final int backLeftTurnCanId = 4; - public static final int frontRightTurnCanId = 8; - public static final int backRightTurnCanId = 6; + public static final int frontLeftTurnCanId = 3; + public static final int backLeftTurnCanId = 5; + public static final int frontRightTurnCanId = 1; + public static final int backRightTurnCanId = 7; - // Drive motor configuration - public static final int driveMotorCurrentLimit = 50; - public static final double wheelRadiusMeters = Units.inchesToMeters(1.5); - public static final double driveMotorReduction = - (45.0 * 22.0) / (14.0 * 15.0); // MAXSwerve with 14 pinion teeth and 22 spur teeth - public static final DCMotor driveGearbox = DCMotor.getNeoVortex(1); + // Drive motor configuration (buttercup: NEO on SparkMax, 6.75:1, 4 in wheel) + public static final int driveMotorCurrentLimit = 40; + public static final double wheelRadiusMeters = Units.inchesToMeters(2.0); + public static final double driveMotorReduction = 6.75; + public static final DCMotor driveGearbox = DCMotor.getNEO(1); // Drive encoder configuration public static final double driveEncoderPositionFactor = @@ -60,8 +60,9 @@ public class DriveConstants { public static final double driveEncoderVelocityFactor = (2 * Math.PI) / 60.0 / driveMotorReduction; // Rotor RPM -> Wheel Rad/Sec - // Drive PID configuration - public static final double driveKp = 0.0; + // Drive PID configuration (driveKp from YAGSL pidfproperties; note YAGSL's native-unit gain is + // not directly equivalent to this rad/s voltage loop, so verify/tune on the robot) + public static final double driveKp = 0.0020645; public static final double driveKd = 0.0; public static final double driveKs = 0.0; public static final double driveKv = 0.1; @@ -70,11 +71,11 @@ public class DriveConstants { public static final double driveSimKs = 0.0; public static final double driveSimKv = 0.0789; - // Turn motor configuration + // Turn motor configuration (buttercup: NEO on SparkMax, 25:1, Canandmag via Spark abs encoder) public static final boolean turnInverted = false; public static final int turnMotorCurrentLimit = 20; - public static final double turnMotorReduction = 9424.0 / 203.0; - public static final DCMotor turnGearbox = DCMotor.getNeo550(1); + public static final double turnMotorReduction = 25.0; + public static final DCMotor turnGearbox = DCMotor.getNEO(1); // Turn encoder configuration public static final boolean turnEncoderInverted = true; @@ -90,9 +91,9 @@ public class DriveConstants { public static final double turnPIDMaxInput = 2 * Math.PI; // Radians // PathPlanner configuration - public static final double robotMassKg = 74.088; + public static final double robotMassKg = 75.0; public static final double robotMOI = 6.883; - public static final double wheelCOF = 1.2; + public static final double wheelCOF = 1.19; public static final RobotConfig ppConfig = new RobotConfig( robotMassKg, diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java index 8e6f652e..af95baf3 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java @@ -50,11 +50,9 @@ import com.revrobotics.spark.SparkBase.ControlType; import com.revrobotics.spark.SparkClosedLoopController; import com.revrobotics.spark.SparkClosedLoopController.ArbFFUnits; -import com.revrobotics.spark.SparkFlex; 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.MathUtil; import edu.wpi.first.math.filter.Debouncer; @@ -98,7 +96,7 @@ public ModuleIOSpark(int module) { default -> new Rotation2d(); }; driveSpark = - new SparkFlex( + new SparkMax( switch (module) { case 0 -> frontLeftDriveCanId; case 1 -> frontRightDriveCanId; @@ -123,7 +121,7 @@ public ModuleIOSpark(int module) { turnController = turnSpark.getClosedLoopController(); // Configure drive motor - var driveConfig = new SparkFlexConfig(); + var driveConfig = new SparkMaxConfig(); driveConfig .idleMode(IdleMode.kBrake) .smartCurrentLimit(driveMotorCurrentLimit) From ac069c23e7cf9b33f4613b887c99ab85a72a92a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 22:29:16 +0000 Subject: [PATCH 66/93] Fix akit Spark drivetrain creation: create TalonFX odometry thread for gyro The AKIT "Spark" branch only created the SparkOdometryThread, but GyroIOPigeon2 (the Pigeon2 is a CTRE/Phoenix device) registers its signal on the TalonFXOdometryThread. With that thread never instantiated, TalonFXOdometryThread.getInstance() returned null and the gyro constructor threw an NPE, so the drivetrain failed to create on the real robot. Create the TalonFXOdometryThread in the "Spark" branch too, matching the working "SparkTalon" branch. Simulation was unaffected because its branch already creates the TalonFX thread and uses GyroIOSim. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../frc5010/common/config/json/AKitSwerveDrivetrainJson.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java index fbd61265..0d340fc5 100644 --- a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java +++ b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java @@ -150,6 +150,10 @@ public void createDriveTrain(GenericRobot robot) { new ModuleIOSparkTalon(config, config.BackRight), (pose) -> {}); } else if ("Spark".equals(type)) { + // GyroIOPigeon2 registers its signal on the TalonFX (Phoenix) odometry thread, so that + // thread must exist even though the modules are pure Spark; otherwise + // TalonFXOdometryThread.getInstance() is null and the gyro constructor throws. + TalonFXOdometryThread.createInstance(config); SparkOdometryThread.createInstance(); driveFunctions = new AkitSwerveDrive( From 6300b98eddbc42ce7478beaa036551d3e876aa49 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 22:45:05 +0000 Subject: [PATCH 67/93] Fix odometry crash when gyro and modules sample on different threads On the real robot with the akit "Spark" configuration, the modules sample on the started SparkOdometryThread (so sampleCount > 0) while the Pigeon2 registers on the Phoenix thread, which is not the started thread - leaving gyroInputs.odometryYawPositions empty. AkitSwerveDrive.periodic then indexed that empty array (odometryYawPositions[i]) and threw ArrayIndexOutOfBoundsException, crashing the robot program after the drivetrain was created. Guard the gyro-sample access: when no per-sample yaw is available for index i, fall back to the latest gyro reading (gyroInputs.yawPosition) so heading stays gyro-based without indexing out of bounds. This is a no-op when the arrays align (e.g. the TalonFX path), so it is safe for all configurations. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../common/drive/swerve/akit/AkitSwerveDrive.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java index 155fc8be..3deaa76f 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java @@ -171,8 +171,15 @@ public void periodic() { // Update gyro angle if (gyroInputs.connected) { - // Use the real gyro angle - rawGyroRotation = gyroInputs.odometryYawPositions[i]; + // Use the real gyro angle. When the gyro samples on a different odometry thread than the + // modules (e.g. the Spark configuration, where the modules use the Spark thread but the + // Pigeon uses the Phoenix thread), its high-frequency queue can be empty even though the + // modules produced samples. Fall back to the latest gyro reading for those samples instead + // of indexing past the end of the array. + rawGyroRotation = + i < gyroInputs.odometryYawPositions.length + ? gyroInputs.odometryYawPositions[i] + : gyroInputs.yawPosition; } else { // Use the angle delta from the kinematics and module deltas Twist2d twist = kinematics.toTwist2d(moduleDeltas); From 16662c584dda64d95e093dd0a1acb89d052aea5d Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 22 Jun 2026 18:50:22 -0400 Subject: [PATCH 68/93] Use the sparkodometry thread as a hack --- .../frc5010/common/config/json/AKitSwerveDrivetrainJson.java | 2 +- .../org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java index 0d340fc5..9592101d 100644 --- a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java +++ b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java @@ -153,7 +153,7 @@ public void createDriveTrain(GenericRobot robot) { // GyroIOPigeon2 registers its signal on the TalonFX (Phoenix) odometry thread, so that // thread must exist even though the modules are pure Spark; otherwise // TalonFXOdometryThread.getInstance() is null and the gyro constructor throws. - TalonFXOdometryThread.createInstance(config); + SparkOdometryThread.createInstance(); driveFunctions = new AkitSwerveDrive( diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java index fd311bd8..1ade6228 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/GyroIOPigeon2.java @@ -36,8 +36,9 @@ public GyroIOPigeon2(AkitSwerveConfig config) { yaw.setUpdateFrequency(config.ODOMETRY_FREQUENCY); yawVelocity.setUpdateFrequency(50.0); pigeon.optimizeBusUtilization(); - yawTimestampQueue = TalonFXOdometryThread.getInstance().makeTimestampQueue(); - yawPositionQueue = TalonFXOdometryThread.getInstance().registerSignal(yaw.clone()); + yawTimestampQueue = SparkOdometryThread.getInstance().makeTimestampQueue(); + yawPositionQueue = + SparkOdometryThread.getInstance().registerSignal(() -> yaw.clone().getValueAsDouble()); } @Override From 24d3868e2c71dcb594b7a88e14a0225a4f5b69b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 23:00:09 +0000 Subject: [PATCH 69/93] Fix no-strafe: populate turnPosition from the absolute encoder (Spark) ModuleIOSpark only set inputs.turnAbsolutePosition and never set inputs.turnPosition, which defaults to zero. On the real robot Module.getAngle() and Module.runSetpoint's cosineScale() use turnPosition, so the module looked like it was always at 0 degrees. The drive speed was then scaled by cos(targetAngle - 0): - forward (0 deg): cos 0 = 1.0 -> full drive - rotate (~45 deg): cos 45 ~= 0.7 -> drives - strafe (90 deg): cos 90 = 0.0 -> no drive so the wheels rotated sideways but never spun, and reported module angles (odometry) were stuck at 0. These modules steer on the absolute encoder with no separate relative turn encoder, so mirror turnPosition from turnAbsolutePosition. Simulation was unaffected because getAngle()/cosineScale use turnAbsolutePosition in sim. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../frc5010/common/drive/swerve/akit/ModuleIOSpark.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java index af95baf3..9e6afefc 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java @@ -212,7 +212,14 @@ public void updateInputs(ModuleIOInputs inputs) { ifOk( turnSpark, turnEncoder::getPosition, - (value) -> inputs.turnAbsolutePosition = new Rotation2d(value).minus(zeroRotation)); + (value) -> { + inputs.turnAbsolutePosition = new Rotation2d(value).minus(zeroRotation); + // The steering closed loop runs on the absolute encoder and there is no separate relative + // turn encoder, so turnPosition mirrors the absolute position. Without this, turnPosition + // stays at 0, which makes Module.getAngle()/cosineScale think the wheel is always at 0 - + // scaling drive speed by cos(targetAngle) and producing no drive for 90 deg (strafe). + inputs.turnPosition = inputs.turnAbsolutePosition; + }); ifOk(turnSpark, turnEncoder::getVelocity, (value) -> inputs.turnVelocityRadPerSec = value); ifOk( turnSpark, From 5eb8b7049c94c25b5e66779a6538c7fcb5893d24 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Mon, 22 Jun 2026 19:16:12 -0400 Subject: [PATCH 70/93] Change rotation --- .../buttercup_swerve/controllers/axis/driver_right_x.json | 2 +- .../frc5010/common/config/json/AKitSwerveDrivetrainJson.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json index 8f981e23..007abd6c 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json @@ -1,7 +1,7 @@ { "channel": 4, "deadband": 0.07, - "invert": true, + "invert": false, "scale": 1.0, "curvePower": 3, "limit": 1.0 diff --git a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java index 9592101d..fecf7404 100644 --- a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java +++ b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java @@ -153,7 +153,7 @@ public void createDriveTrain(GenericRobot robot) { // GyroIOPigeon2 registers its signal on the TalonFX (Phoenix) odometry thread, so that // thread must exist even though the modules are pure Spark; otherwise // TalonFXOdometryThread.getInstance() is null and the gyro constructor throws. - + SparkOdometryThread.createInstance(); driveFunctions = new AkitSwerveDrive( From cf6a9862f17c991d69a16d69979dd7db29e1c5d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 15:55:05 +0000 Subject: [PATCH 71/93] Add CLAUDE.md with build commands and architecture overview Documents the Spotless/JDK build workaround, the JSON-config-driven multi-robot selection flow, and the swerve drivetrain architecture (YAGSL vs AKit, the Spark IO reading DriveConstants instead of JSON, the dual odometry threads, and absolute-encoder steering pitfalls). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- CLAUDE.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d4102b64 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +FRC team 5010's 2026 robot code (WPILib, Java 17, command-based). It is built on a reusable in-house framework (`org.frc5010.common`, "FRC5010Lib") that is **JSON-configuration-driven**: robots, subsystems, drivetrains, controllers, and cameras are described by deploy-time JSON rather than hardcoded, so one codebase runs multiple physical robots. AdvantageKit (`org.littletonrobotics.junction`) provides logging/replay; the `Robot` class extends `LoggedRobot`. + +## Build / run commands + +The build runs Spotless (`compileJava.dependsOn(spotlessApply)`), and Spotless's `googleJavaFormat()` crashes on newer JDKs in some environments (`NoSuchMethodError ... JCImport.getQualifiedIdentifier`). When that happens, exclude the Spotless tasks: + +```bash +./gradlew compileJava -x spotlessJava -x spotlessJavaCheck # compile only +./gradlew build -x spotlessJava -x spotlessJavaCheck # full build +./gradlew test -x spotlessJava -x spotlessJavaCheck # JUnit 5 tests (src/test/java) +./gradlew test --tests 'org.frc5010.common.arch.GenericRobotTest' # single test class +./gradlew simulateJavaRelease # desktop/maple-sim simulation +./gradlew deploy # deploy to the roboRIO +``` + +Spotless config: `googleJavaFormat`, `removeUnusedImports`, 100-col limit. Run `./gradlew spotlessApply` locally before pushing where the toolchain works; otherwise match the style by hand (2-space indent, ≤100 col code lines — comments/Javadoc are left alone by the formatter). + +Chromium/Playwright are preinstalled in the cloud env; do not run `playwright install`. + +## How a robot is selected and built (read these together) + +1. `frc.robot.Main` → `frc.robot.Robot` (`LoggedRobot`) → `RobotContainer` → `RobotsParser`. +2. `RobotsParser` reads `src/main/deploy/robots.json`, which maps a robot **name → { id (roboRIO MAC), robotClass, simulate, competition }**. The active robot is chosen by matching the RIO's MAC, or in simulation by the `simulate: true` flag. The matching entry's `robotClass` (e.g. `frc.robot.blackteam.BlackRobot`) is instantiated via reflection with its **deploy directory name** (e.g. `buttercup_swerve`). +3. `GenericRobot(String directory)` constructs `RobotParser` + `SubsystemParser`, which read the JSON files under `src/main/deploy//` and build the drivetrain, subsystems, cameras, and controllers. **Exceptions during this phase are caught in `GenericRobot`'s constructor, printed, and swallowed** — a failed parse leaves a half-built robot rather than a crash, so "the drivetrain didn't get created" usually means an exception was thrown and logged during config parsing. + +So robot behavior is determined largely by `src/main/deploy//*.json`, not by Java edits. The two configured robots share `BlackRobot` but have different deploy dirs (`black_robot`, `buttercup_swerve`). + +## Code layout + +- `src/main/java/org/frc5010/common/` — the framework library: + - `arch/` — `GenericRobot`, `GenericSubsystem`, `GenericCommand`, NetworkTables value plumbing. + - `config/` — JSON parsers (`RobotParser`, `SubsystemParser`, `RobotsParser`) and the `config/json/**` POJOs that map 1:1 to the deploy JSON. `UnitsParser` + `config/units/**` parse `{ "val", "uom" }` measurements (units accept aliases, e.g. `m/sec`, `kg*m^2`, `degrees`). + - `drive/` — drivetrain abstractions. See the swerve section below. + - `telemetry/` — `DisplayValuesHelper` + `Display*` types wrap NetworkTables entries; `makeConfig*` values are persistent and dashboard-editable (used for live tuning). + - `sensors/`, `motors/`, `mechanisms/`, `subsystems/`, `vision/`, `auto/`, `commands/`. +- `src/main/java/frc/robot/` — WPILib entrypoint + robot-specific code; `blackteam/BlackRobot` is the concrete `GenericRobot` and its subsystems (flywheels, feeder). +- `src/main/deploy//` — per-robot JSON config (robot.json, drivetrain, subsystems, controllers, cameras, pathplanner). +- `src/main/resources/schemas/` — JSON schemas for the config files (e.g. `akit_swerve_drivetrain.schema.json`); validate config changes against these. + +## Swerve drivetrain architecture (the most complex area) + +`GenericSwerveDrivetrain` (the WPILib `Subsystem`) wraps a `SwerveDriveFunctions` implementation chosen by `robot.json`'s `driveType`: + +- `YAGSL_SWERVE_DRIVE` → `YAGSLSwerveDrivetrain` (the YAGSL/`swervelib` library). Configured by `deploy//yagsl_swerve/**` JSON (modules, `pidfproperties.json`, `physicalproperties.json`, `swervedrive.json`). +- `AKIT_SWERVE_DRIVE` → `AkitSwerveDrive` (AdvantageKit-style IO-layer swerve). Configured by `deploy//akit_swerve_drivetrain.json` (validated by the schema). `type` selects the module IO: `TalonFX`, `SparkTalon` (TalonFX drive + Spark steer), or `Spark` (NEO/SparkMax drive + steer). + +Key AKit-swerve facts that are non-obvious and have caused real bugs: + +- **The `Spark` IO ignores most of the JSON** — `ModuleIOSpark` reads CAN IDs, gear ratios, drive PID, current limits, and per-module `zeroRotation` from the hardcoded `drive/swerve/akit/DriveConstants.java`, **not** from `akit_swerve_drivetrain.json`. The JSON still drives kinematics/PathPlanner/sim. To change Spark hardware behavior, edit `DriveConstants`. (`SparkTalon`/`TalonFX` IOs do read the JSON via `AkitSwerveConfig`.) +- **Two odometry threads.** `ModuleIOSpark`/`ModuleIOSparkTalon` sample on `SparkOdometryThread`; `GyroIOPigeon2` (a CTRE device) samples on `TalonFXOdometryThread`. Both must be created for the gyro to work, and `AkitSwerveDrive.periodic` must tolerate the two queues having different lengths. +- **Steering runs closed-loop on the absolute encoder.** Canandmags are read via the Spark's absolute-encoder API; offsets rely on the encoder's internal zero (config offsets are `0`). `Module.runSetpoint` cosine-scales drive speed by the *measured* steering angle and `optimize()`s against it, so the module IO must populate `inputs.turnPosition` (not just `turnAbsolutePosition`) or strafe/odometry break. A module whose zero leaves it resting at ±180° sits on the wrap seam and will intermittently flip direction. +- Simulation skips the hardware IO entirely (uses `ModuleIOSim`/`GyroIOSim` + maple-sim) and never starts the odometry threads, so hardware-thread/encoder bugs do **not** reproduce in sim. + +## Git / workflow + +Active development branches are created per-task off the team's feature branches (e.g. `claude/...`). The default branch is protected — branch before committing. Do not push to a different branch without explicit permission. GitHub operations go through the `mcp__github__*` tools (scope: `frc5010/teamblack2025`); there is no `gh` CLI. From c85009d785ec55060d001635ea428b2599a86f42 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 16:38:22 +0000 Subject: [PATCH 72/93] Make Spark IO config-driven, add azimuth offset support and swerve diagnostics ModuleIOSpark now reads its hardware identity from the deploy JSON (AkitSwerveConfig / SwerveModuleConstants) instead of DriveConstants: - drive/steer CAN IDs, drive gear ratio, drive current limit - drive/steer motor inversion and absolute-encoder inversion - absolute encoder zero offset (JSON absoluteOffset -> EncoderOffset -> zeroRotation), so per-module offsets can finally be set from config to move modules off the +/-180 wrap seam. Closed-loop gains and the steer wrapping stay in DriveConstants because they are in AdvantageKit units (wheel rad/s, module radians), not the YAGSL-native gains stored elsewhere in the JSON. Diagnostics for swerve commissioning: - ModuleIO gains turnRawAbsolutePosition (pre-offset encoder reading). Point a wheel forward and copy its logged degrees into the module's absoluteOffset. - AkitSwerveDrive logs Drive/Diag/Module*/{Setpoint,Measured,AngleError}* and chassis speeds setpoint/measured every loop, independent of enable state, so wrong-direction and tracking issues are visible even in disabled logs (where SwerveStates/Setpoints is blanked). Spark branch now constructs ModuleIOSpark(config, moduleConstants). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../config/json/AKitSwerveDrivetrainJson.java | 14 ++- .../drive/swerve/akit/AkitSwerveDrive.java | 38 ++++++++ .../common/drive/swerve/akit/ModuleIO.java | 2 + .../drive/swerve/akit/ModuleIOSpark.java | 87 ++++++++----------- 4 files changed, 81 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java index fecf7404..7ecd3ee2 100644 --- a/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java +++ b/src/main/java/org/frc5010/common/config/json/AKitSwerveDrivetrainJson.java @@ -150,19 +150,17 @@ public void createDriveTrain(GenericRobot robot) { new ModuleIOSparkTalon(config, config.BackRight), (pose) -> {}); } else if ("Spark".equals(type)) { - // GyroIOPigeon2 registers its signal on the TalonFX (Phoenix) odometry thread, so that - // thread must exist even though the modules are pure Spark; otherwise - // TalonFXOdometryThread.getInstance() is null and the gyro constructor throws. - + // GyroIOPigeon2 samples on the Spark odometry thread in this configuration, so only that + // thread needs to be created. SparkOdometryThread.createInstance(); driveFunctions = new AkitSwerveDrive( config, new GyroIOPigeon2(config), - new ModuleIOSpark(0), - new ModuleIOSpark(1), - new ModuleIOSpark(2), - new ModuleIOSpark(3), + new ModuleIOSpark(config, config.FrontLeft), + new ModuleIOSpark(config, config.FrontRight), + new ModuleIOSpark(config, config.BackLeft), + new ModuleIOSpark(config, config.BackRight), (pose) -> {}); } else if ("TalonFX".equals(type)) { TalonFXOdometryThread.createInstance(config); diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java index 3deaa76f..5829ed74 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java @@ -81,6 +81,17 @@ public class AkitSwerveDrive extends SwerveDriveFunctions { private SwerveDrivePoseEstimator poseEstimator; private final Consumer resetSimulationPoseCallBack; + // Diagnostics: last commanded (optimized) module setpoints and chassis speeds. Logged every loop + // so setpoint-vs-measured is visible even while disabled, when SwerveStates/Setpoints is blanked. + private SwerveModuleState[] lastSetpointStates = + new SwerveModuleState[] { + new SwerveModuleState(), + new SwerveModuleState(), + new SwerveModuleState(), + new SwerveModuleState() + }; + private ChassisSpeeds lastSetpointSpeeds = new ChassisSpeeds(); + public AkitSwerveDrive( AkitSwerveConfig config, GyroIO gyroIO, @@ -192,6 +203,9 @@ public void periodic() { // Update gyro alert gyroDisconnectedAlert.set(!gyroInputs.connected && Constants.CURRENT_MODE != Mode.SIM); + + // Always-on swerve diagnostics (commanded vs measured per module). + logModuleDiagnostics(); } /** @@ -216,6 +230,30 @@ public void runVelocity(ChassisSpeeds speeds) { // Log optimized setpoints (runSetpoint mutates each state) Logger.recordOutput("SwerveStates/SetpointsOptimized", setpointStates); + + // Retain the commanded setpoints for the always-on diagnostics in periodic(). + lastSetpointStates = setpointStates; + lastSetpointSpeeds = speeds; + } + + /** + * Logs per-module commanded-vs-measured azimuth and drive diagnostics every loop, independent of + * enable state, to make wrong-direction / tracking issues diagnosable from a log. + */ + private void logModuleDiagnostics() { + for (int i = 0; i < 4; i++) { + SwerveModuleState measured = modules[i].getState(); + SwerveModuleState setpoint = lastSetpointStates[i]; + double angleErrorDeg = Math.abs(measured.angle.minus(setpoint.angle).getDegrees()); + String base = "Drive/Diag/Module" + i + "/"; + Logger.recordOutput(base + "SetpointAngleDeg", setpoint.angle.getDegrees()); + Logger.recordOutput(base + "MeasuredAngleDeg", measured.angle.getDegrees()); + Logger.recordOutput(base + "AngleErrorDeg", angleErrorDeg); + Logger.recordOutput(base + "SetpointSpeedMps", setpoint.speedMetersPerSecond); + Logger.recordOutput(base + "MeasuredSpeedMps", measured.speedMetersPerSecond); + } + Logger.recordOutput("Drive/Diag/ChassisSpeedsSetpoint", lastSetpointSpeeds); + Logger.recordOutput("Drive/Diag/ChassisSpeedsMeasured", getChassisSpeeds()); } /** Runs the drive in a straight line with the specified drive output. */ diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java index d4199b10..3ea71de1 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java @@ -24,6 +24,8 @@ class ModuleIOInputs { public boolean turnEncoderConnected = false; public Rotation2d turnPosition = Rotation2d.kZero; public Rotation2d turnAbsolutePosition = Rotation2d.kZero; + /** Absolute encoder reading before the configured zero offset is applied (diagnostics). */ + public Rotation2d turnRawAbsolutePosition = Rotation2d.kZero; public double turnVelocityRadPerSec = 0.0; public double turnAppliedVolts = 0.0; public double turnCurrentAmps = 0.0; diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java index 9e6afefc..6685407a 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java @@ -7,30 +7,13 @@ package org.frc5010.common.drive.swerve.akit; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.backLeftDriveCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.backLeftTurnCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.backLeftZeroRotation; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.backRightDriveCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.backRightTurnCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.backRightZeroRotation; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveEncoderPositionFactor; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveEncoderVelocityFactor; import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKd; import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKp; import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKs; import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKv; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveMotorCurrentLimit; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontLeftDriveCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontLeftTurnCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontLeftZeroRotation; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontRightDriveCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontRightTurnCanId; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.frontRightZeroRotation; import static org.frc5010.common.drive.swerve.akit.DriveConstants.odometryFrequency; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderInverted; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderPositionFactor; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderVelocityFactor; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnInverted; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKd; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKp; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnMotorCurrentLimit; @@ -40,6 +23,9 @@ import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.sparkStickyFault; import static org.frc5010.common.drive.swerve.akit.util.SparkUtil.tryUntilOk; +import com.ctre.phoenix6.configs.CANcoderConfiguration; +import com.ctre.phoenix6.configs.TalonFXConfiguration; +import com.ctre.phoenix6.swerve.SwerveModuleConstants; import com.revrobotics.AbsoluteEncoder; import com.revrobotics.PersistMode; import com.revrobotics.RelativeEncoder; @@ -59,12 +45,21 @@ import edu.wpi.first.math.geometry.Rotation2d; import java.util.Queue; import java.util.function.DoubleSupplier; +import org.frc5010.common.drive.swerve.AkitSwerveConfig; /** - * Module IO implementation for Spark Flex drive motor controller, Spark Max turn motor controller, - * and duty cycle absolute encoder. + * Module IO implementation for a NEO (Spark Max) drive motor, NEO (Spark Max) turn motor, and an + * absolute encoder (e.g. Canandmag) read through the turn Spark's absolute-encoder port. + * + *

Hardware identity is read from the deploy JSON (via {@link AkitSwerveConfig} / + * {@link SwerveModuleConstants}): drive/steer CAN IDs, drive gear ratio, drive current limit, the + * motor/encoder inversions, and the absolute encoder zero offset. The closed-loop gains and the + * steer feedback wrapping remain in {@link DriveConstants} because they are expressed in + * AdvantageKit units (wheel rad/s, module radians) that are not interchangeable with the + * YAGSL-native gains stored elsewhere in the JSON. */ public class ModuleIOSpark implements ModuleIO { + /** Absolute encoder zero offset, from the JSON {@code absoluteOffset}. */ private final Rotation2d zeroRotation; // Hardware objects @@ -86,35 +81,18 @@ public class ModuleIOSpark implements ModuleIO { private final Debouncer driveConnectedDebounce = new Debouncer(0.5); private final Debouncer turnConnectedDebounce = new Debouncer(0.5); - public ModuleIOSpark(int module) { - zeroRotation = - switch (module) { - case 0 -> frontLeftZeroRotation; - case 1 -> frontRightZeroRotation; - case 2 -> backLeftZeroRotation; - case 3 -> backRightZeroRotation; - default -> new Rotation2d(); - }; - driveSpark = - new SparkMax( - switch (module) { - case 0 -> frontLeftDriveCanId; - case 1 -> frontRightDriveCanId; - case 2 -> backLeftDriveCanId; - case 3 -> backRightDriveCanId; - default -> 0; - }, - MotorType.kBrushless); - turnSpark = - new SparkMax( - switch (module) { - case 0 -> frontLeftTurnCanId; - case 1 -> frontRightTurnCanId; - case 2 -> backLeftTurnCanId; - case 3 -> backRightTurnCanId; - default -> 0; - }, - MotorType.kBrushless); + public ModuleIOSpark( + AkitSwerveConfig config, + SwerveModuleConstants + constants) { + zeroRotation = Rotation2d.fromRotations(constants.EncoderOffset); + + // Rotor rotations -> wheel radians, using the JSON drive gear ratio. + double driveEncoderPositionFactor = 2 * Math.PI / constants.DriveMotorGearRatio; + double driveEncoderVelocityFactor = (2 * Math.PI) / 60.0 / constants.DriveMotorGearRatio; + + driveSpark = new SparkMax(constants.DriveMotorId, MotorType.kBrushless); + turnSpark = new SparkMax(constants.SteerMotorId, MotorType.kBrushless); driveEncoder = driveSpark.getEncoder(); turnEncoder = turnSpark.getAbsoluteEncoder(); driveController = driveSpark.getClosedLoopController(); @@ -123,8 +101,9 @@ public ModuleIOSpark(int module) { // Configure drive motor var driveConfig = new SparkMaxConfig(); driveConfig + .inverted(constants.DriveMotorInverted) .idleMode(IdleMode.kBrake) - .smartCurrentLimit(driveMotorCurrentLimit) + .smartCurrentLimit((int) constants.SlipCurrent) .voltageCompensation(12.0); driveConfig .encoder @@ -153,13 +132,13 @@ public ModuleIOSpark(int module) { // Configure turn motor var turnConfig = new SparkMaxConfig(); turnConfig - .inverted(turnInverted) + .inverted(constants.SteerMotorInverted) .idleMode(IdleMode.kBrake) .smartCurrentLimit(turnMotorCurrentLimit) .voltageCompensation(12.0); turnConfig .absoluteEncoder - .inverted(turnEncoderInverted) + .inverted(constants.EncoderInverted) .positionConversionFactor(turnEncoderPositionFactor) .velocityConversionFactor(turnEncoderVelocityFactor) .averageDepth(2); @@ -213,7 +192,11 @@ public void updateInputs(ModuleIOInputs inputs) { turnSpark, turnEncoder::getPosition, (value) -> { - inputs.turnAbsolutePosition = new Rotation2d(value).minus(zeroRotation); + Rotation2d raw = new Rotation2d(value); + // Raw reading is logged for offset calibration: with the wheel pointed forward, the + // reported raw angle (deg) is the value to copy into the module's JSON absoluteOffset. + inputs.turnRawAbsolutePosition = raw; + inputs.turnAbsolutePosition = raw.minus(zeroRotation); // The steering closed loop runs on the absolute encoder and there is no separate relative // turn encoder, so turnPosition mirrors the absolute position. Without this, turnPosition // stays at 0, which makes Module.getAngle()/cosineScale think the wheel is always at 0 - From 407ab9bedcc9edf0284dbec2e169b9878e4c96ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 17:07:54 +0000 Subject: [PATCH 73/93] Read Spark PID from JSON; fix black to Spark akit and switch it to AKIT ModuleIOSpark now reads the drive and steer closed-loop gains from the deploy JSON (constants.DriveMotorGains / SteerMotorGains, which AkitSwerveConfig builds from driveMotorControl / steerMotorControl) instead of DriveConstants. The akit drivetrain owns its own akit-unit PID; it is not the YAGSL pidfproperties gain. Only universal akit constants (encoder factors, steer wrap range, steer current, odometry frequency) remain in DriveConstants. Buttercup JSON now holds proper akit-unit gains (steer kP 2.0, drive kV 0.1) rather than the YAGSL copies (steer p 0.02). Black akit config was {"type":"SparkTalon"}, which instantiates a TalonFX drive motor - wrong for black's NEO-on-SparkMax hardware. Set type "Spark" with a full constants block from black's YAGSL config (CAN IDs FL 1/2, FR 7/8, BL 3/4, BR 5/6; drive inverted -> invertLeftSide/invertRightSide true; encoderInverted true). Switch black_robot/robot.json driveType to AKIT_SWERVE_DRIVE. Black's absoluteOffsets are still 0 and will need azimuth-offset calibration (point wheels forward, read Drive/Module*/TurnRawAbsolutePosition) before it drives cleanly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../black_robot/akit_swerve_drivetrain.json | 221 +++++++++++++++++- src/main/deploy/black_robot/robot.json | 2 +- .../akit_swerve_drivetrain.json | 4 +- .../drive/swerve/akit/ModuleIOSpark.java | 32 +-- 4 files changed, 241 insertions(+), 18 deletions(-) diff --git a/src/main/deploy/black_robot/akit_swerve_drivetrain.json b/src/main/deploy/black_robot/akit_swerve_drivetrain.json index 42f49455..328e047b 100644 --- a/src/main/deploy/black_robot/akit_swerve_drivetrain.json +++ b/src/main/deploy/black_robot/akit_swerve_drivetrain.json @@ -1,3 +1,222 @@ { - "type": "SparkTalon" + "type": "Spark", + "constants": { + "trackWidth": { + "val": 18.5625, + "uom": "inches" + }, + "wheelBase": { + "val": 18.5625, + "uom": "inches" + }, + "wheelDiameter": { + "val": 4, + "uom": "inches" + }, + "maxDriveSpeed": { + "val": 5.93, + "uom": "m/sec" + }, + "bumperFrameLength": { + "val": 28, + "uom": "inches" + }, + "bumperFrameWidth": { + "val": 28, + "uom": "inches" + }, + "gyro": { + "type": "pigeon2", + "id": 9, + "inverted": false, + "canbus": "" + }, + "driveGearRatio": "1:6.75", + "steerGearRatio": "1:25", + "driveMotorControl": { + "closedLoopRamp": { + "val": 0.15, + "uom": "seconds" + }, + "feedBack": { + "p": 0.0020645, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.1, + "a": 0.0 + } + }, + "steerMotorControl": { + "closedLoopRamp": { + "val": 0.05, + "uom": "seconds" + }, + "feedBack": { + "p": 2.0, + "i": 0.0, + "d": 0.0 + }, + "feedForward": { + "s": 0.0, + "v": 0.0, + "a": 0.0 + } + }, + "modules": { + "frontLeft": { + "driveMotorSetup": { + "name": "frontLeftDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 1, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "frontLeftSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 2, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + }, + "frontRight": { + "driveMotorSetup": { + "name": "frontRightDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 7, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "frontRightSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 8, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + }, + "backLeft": { + "driveMotorSetup": { + "name": "backLeftDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 3, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "backLeftSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 4, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + }, + "backRight": { + "driveMotorSetup": { + "name": "backRightDrive", + "controllerType": "spark", + "motorType": "NEO", + "canId": 5, + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "steerMotorSetup": { + "name": "backRightSteer", + "controllerType": "spark", + "motorType": "NEO", + "canId": 6, + "inverted": false, + "currentLimit": { + "val": 20, + "uom": "amps" + } + }, + "encoderId": 0, + "absoluteOffset": { + "val": 0, + "uom": "degrees" + }, + "encoderInverted": true + } + }, + "coupleRatio": 1.0, + "invertLeftSide": true, + "invertRightSide": true, + "steerInertia": { + "val": 0.04, + "uom": "kg*m^2" + }, + "driveInertia": { + "val": 0.025, + "uom": "kg*m^2" + }, + "robotMass": { + "val": 75, + "uom": "kg" + }, + "wheelCOF": 1.19, + "slipCurrent": { + "val": 40, + "uom": "amps" + }, + "canbus": "", + "startingPose": { + "x": { + "val": 1.0, + "uom": "m" + }, + "y": { + "val": 1.0, + "uom": "m" + }, + "rotation": { + "val": 0.0, + "uom": "deg" + } + } + } } diff --git a/src/main/deploy/black_robot/robot.json b/src/main/deploy/black_robot/robot.json index 086c07a3..eeac1fa3 100644 --- a/src/main/deploy/black_robot/robot.json +++ b/src/main/deploy/black_robot/robot.json @@ -1,6 +1,6 @@ { "userConfig": "demo_mode.json", - "driveType": "YAGSL_SWERVE_DRIVE", + "driveType": "AKIT_SWERVE_DRIVE", "trackWidth": 18.5625, "trackWidthUom": "in", "wheelBase": 18.5625, diff --git a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json index 355e07c5..d9917d71 100644 --- a/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json +++ b/src/main/deploy/buttercup_swerve/akit_swerve_drivetrain.json @@ -45,7 +45,7 @@ }, "feedForward": { "s": 0.0, - "v": 0.0, + "v": 0.1, "a": 0.0 } }, @@ -55,7 +55,7 @@ "uom": "seconds" }, "feedBack": { - "p": 0.02, + "p": 2.0, "i": 0.0, "d": 0.0 }, diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java index 6685407a..aa572330 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java @@ -7,15 +7,9 @@ package org.frc5010.common.drive.swerve.akit; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKd; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKp; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKs; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.driveKv; import static org.frc5010.common.drive.swerve.akit.DriveConstants.odometryFrequency; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderPositionFactor; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderVelocityFactor; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKd; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKp; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnMotorCurrentLimit; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMaxInput; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMinInput; @@ -51,17 +45,21 @@ * Module IO implementation for a NEO (Spark Max) drive motor, NEO (Spark Max) turn motor, and an * absolute encoder (e.g. Canandmag) read through the turn Spark's absolute-encoder port. * - *

Hardware identity is read from the deploy JSON (via {@link AkitSwerveConfig} / + *

Everything per-robot is read from the deploy JSON (via {@link AkitSwerveConfig} / * {@link SwerveModuleConstants}): drive/steer CAN IDs, drive gear ratio, drive current limit, the - * motor/encoder inversions, and the absolute encoder zero offset. The closed-loop gains and the - * steer feedback wrapping remain in {@link DriveConstants} because they are expressed in - * AdvantageKit units (wheel rad/s, module radians) that are not interchangeable with the - * YAGSL-native gains stored elsewhere in the JSON. + * motor/encoder inversions, the absolute encoder zero offset, and the drive/steer closed-loop gains + * ({@code driveMotorControl}/{@code steerMotorControl} in akit units). Only universal akit constants + * (encoder conversion factors, the steer wrap range, steer current limit, odometry frequency) come + * from {@link DriveConstants}. */ public class ModuleIOSpark implements ModuleIO { /** Absolute encoder zero offset, from the JSON {@code absoluteOffset}. */ private final Rotation2d zeroRotation; + // Drive feedforward gains (from the JSON), used in setDriveVelocity. + private final double driveKs; + private final double driveKv; + // Hardware objects private final SparkBase driveSpark; private final SparkBase turnSpark; @@ -86,6 +84,8 @@ public ModuleIOSpark( SwerveModuleConstants constants) { zeroRotation = Rotation2d.fromRotations(constants.EncoderOffset); + driveKs = constants.DriveMotorGains.kS; + driveKv = constants.DriveMotorGains.kV; // Rotor rotations -> wheel radians, using the JSON drive gear ratio. double driveEncoderPositionFactor = 2 * Math.PI / constants.DriveMotorGearRatio; @@ -111,7 +111,11 @@ public ModuleIOSpark( .velocityConversionFactor(driveEncoderVelocityFactor) .uvwMeasurementPeriod(10) .uvwAverageDepth(2); - driveConfig.closedLoop.feedbackSensor(FeedbackSensor.kPrimaryEncoder).p(driveKp).d(driveKd); + driveConfig + .closedLoop + .feedbackSensor(FeedbackSensor.kPrimaryEncoder) + .p(constants.DriveMotorGains.kP) + .d(constants.DriveMotorGains.kD); driveConfig .signals .primaryEncoderPositionAlwaysOn(true) @@ -147,8 +151,8 @@ public ModuleIOSpark( .feedbackSensor(FeedbackSensor.kAbsoluteEncoder) .positionWrappingEnabled(true) .positionWrappingInputRange(turnPIDMinInput, turnPIDMaxInput) - .p(turnKp) - .d(turnKd); + .p(constants.SteerMotorGains.kP) + .d(constants.SteerMotorGains.kD); turnConfig .signals .absoluteEncoderPositionAlwaysOn(true) From 606ef53bb6d6c07a88aefd0bf04becdc1750d05d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 22:47:48 +0000 Subject: [PATCH 74/93] Fix ModuleIOSparkTalon: set turnPosition and read steer config from JSON Mirror the ModuleIOSpark fixes onto the SparkTalon steer side: - Populate inputs.turnPosition (= turnAbsolutePosition) and the turnRawAbsolutePosition diagnostic. Without turnPosition, Module.getAngle()/cosineScale saw the wheel as always at 0 deg, so drive was scaled by cos(targetAngle) (no strafe) and odometry module angles were stuck. - Read steer motor inversion, encoder inversion, steer PID, and the absolute encoder offset from the deploy JSON (constants.SteerMotorInverted / EncoderInverted / SteerMotorGains / EncoderOffset) instead of DriveConstants, matching the now config-driven drive side and ModuleIOSpark. - Keep universal akit constants (encoder factors, wrap range, steer current, odometry frequency) in DriveConstants. No deploy config currently selects "SparkTalon", so this is a correctness fix so the type works if selected later. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../drive/swerve/akit/ModuleIOSparkTalon.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java index 32f914ea..e4f8c20f 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSparkTalon.java @@ -8,12 +8,8 @@ package org.frc5010.common.drive.swerve.akit; import static org.frc5010.common.drive.swerve.akit.DriveConstants.odometryFrequency; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderInverted; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderPositionFactor; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnEncoderVelocityFactor; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnInverted; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKd; -import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnKp; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnMotorCurrentLimit; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMaxInput; import static org.frc5010.common.drive.swerve.akit.DriveConstants.turnPIDMinInput; @@ -106,7 +102,7 @@ public ModuleIOSparkTalon( constants) { this.constants = constants; - zeroRotation = new Rotation2d(); + zeroRotation = Rotation2d.fromRotations(constants.EncoderOffset); driveTalon = new TalonFX(constants.DriveMotorId, config.getCANBus()); turnSpark = new SparkMax(constants.SteerMotorId, MotorType.kBrushless); @@ -134,13 +130,13 @@ public ModuleIOSparkTalon( // Configure turn motor var turnConfig = new SparkMaxConfig(); turnConfig - .inverted(turnInverted) + .inverted(constants.SteerMotorInverted) .idleMode(IdleMode.kBrake) .smartCurrentLimit(turnMotorCurrentLimit) .voltageCompensation(12.0); turnConfig .absoluteEncoder - .inverted(turnEncoderInverted) + .inverted(constants.EncoderInverted) .positionConversionFactor(turnEncoderPositionFactor) .velocityConversionFactor(turnEncoderVelocityFactor) .averageDepth(2); @@ -149,8 +145,8 @@ public ModuleIOSparkTalon( .feedbackSensor(FeedbackSensor.kAbsoluteEncoder) .positionWrappingEnabled(true) .positionWrappingInputRange(turnPIDMinInput, turnPIDMaxInput) - .p(turnKp) - .d(turnKd); + .p(constants.SteerMotorGains.kP) + .d(constants.SteerMotorGains.kD); turnConfig .signals .absoluteEncoderPositionAlwaysOn(true) @@ -193,7 +189,15 @@ public void updateInputs(ModuleIOInputs inputs) { ifOk( turnSpark, turnEncoder::getPosition, - (value) -> inputs.turnAbsolutePosition = new Rotation2d(value).minus(zeroRotation)); + (value) -> { + Rotation2d raw = new Rotation2d(value); + inputs.turnRawAbsolutePosition = raw; + inputs.turnAbsolutePosition = raw.minus(zeroRotation); + // Steering closes the loop on the absolute encoder with no separate relative turn + // encoder, so turnPosition mirrors the absolute position. Without this, turnPosition + // stays at 0, breaking Module.getAngle()/cosineScale (no drive at 90 deg) and odometry. + inputs.turnPosition = inputs.turnAbsolutePosition; + }); ifOk(turnSpark, turnEncoder::getVelocity, (value) -> inputs.turnVelocityRadPerSec = value); ifOk( turnSpark, From f7ae8ca0491480f4ea05ac3084d375da76453270 Mon Sep 17 00:00:00 2001 From: 5010 Tiger Dynasty Date: Thu, 25 Jun 2026 18:50:44 -0400 Subject: [PATCH 75/93] Spark Fishers Changes --- simgui.json | 16 ++-- .../controllers/axis/driver_left_trigger.json | 2 +- .../controllers/axis/driver_right_x.json | 2 +- src/main/deploy/black_robot/demo_mode.json | 2 +- .../controllers/axis/driver_left_trigger.json | 2 +- .../controllers/axis/driver_right_x.json | 2 +- .../deploy/buttercup_swerve/demo_mode.json | 2 +- src/main/java/frc/robot/BuildConstants.java | 10 +-- .../java/frc/robot/blackteam/BlackRobot.java | 89 +++++++++++++++---- .../common/drive/GenericDrivetrain.java | 2 +- .../drive/swerve/GenericSwerveDrivetrain.java | 4 +- 11 files changed, 94 insertions(+), 39 deletions(-) diff --git a/simgui.json b/simgui.json index 523b98d6..1e98610d 100644 --- a/simgui.json +++ b/simgui.json @@ -105,6 +105,10 @@ "/SmartDashboard/IMU": "Alerts", "/SmartDashboard/JSON": "Alerts", "/SmartDashboard/LowerShooterMech/mechanism": "Mechanism2d", + "/SmartDashboard/Mechanisms/Commands/FeederMotor/Down": "Command", + "/SmartDashboard/Mechanisms/Commands/FeederMotor/Up": "Command", + "/SmartDashboard/Mechanisms/Commands/FeederMotor/ZeroEncoder": "Command", + "/SmartDashboard/Mechanisms/Commands/FeederSubsystem/Live Tuning": "Command", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/Down": "Command", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/LiveTuning": "Command", "/SmartDashboard/Mechanisms/FeederMech/FeederMotor/Commands/Up": "Command", @@ -714,12 +718,12 @@ "arrowWeight": 3.0, "style": "Hidden" }, - "bottom": 1638, - "height": 8.051901817321777, - "left": 534, - "right": 3466, - "top": 291, - "width": 17.54825210571289, + "bottom": 1914, + "height": 8.069275856018066, + "left": 245, + "right": 3942, + "top": 118, + "width": 16.54119300842285, "window": { "visible": true } diff --git a/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json index f899fd4b..007abd6c 100644 --- a/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json +++ b/src/main/deploy/black_robot/controllers/axis/driver_left_trigger.json @@ -1,5 +1,5 @@ { - "channel": 2, + "channel": 4, "deadband": 0.07, "invert": false, "scale": 1.0, diff --git a/src/main/deploy/black_robot/controllers/axis/driver_right_x.json b/src/main/deploy/black_robot/controllers/axis/driver_right_x.json index 8f981e23..41b79133 100644 --- a/src/main/deploy/black_robot/controllers/axis/driver_right_x.json +++ b/src/main/deploy/black_robot/controllers/axis/driver_right_x.json @@ -1,5 +1,5 @@ { - "channel": 4, + "channel": 2, "deadband": 0.07, "invert": true, "scale": 1.0, diff --git a/src/main/deploy/black_robot/demo_mode.json b/src/main/deploy/black_robot/demo_mode.json index 865cc1b5..a54e8ef0 100644 --- a/src/main/deploy/black_robot/demo_mode.json +++ b/src/main/deploy/black_robot/demo_mode.json @@ -1,5 +1,5 @@ { - "maxSpeed": 0.40, + "maxSpeed": 1.0, "maxAngularSpeed": 1.0, "maxAccelleration": 1.0, "maxAngularAccelleration": 9.0 diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json index 3359502c..97720dc5 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_trigger.json @@ -1,5 +1,5 @@ { - "channel": 2, + "channel": 4, "deadband": 0.07, "invert": false, "scale": 0.5, diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json index 007abd6c..f899fd4b 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_right_x.json @@ -1,5 +1,5 @@ { - "channel": 4, + "channel": 2, "deadband": 0.07, "invert": false, "scale": 1.0, diff --git a/src/main/deploy/buttercup_swerve/demo_mode.json b/src/main/deploy/buttercup_swerve/demo_mode.json index 865cc1b5..a54e8ef0 100644 --- a/src/main/deploy/buttercup_swerve/demo_mode.json +++ b/src/main/deploy/buttercup_swerve/demo_mode.json @@ -1,5 +1,5 @@ { - "maxSpeed": 0.40, + "maxSpeed": 1.0, "maxAngularSpeed": 1.0, "maxAccelleration": 1.0, "maxAngularAccelleration": 9.0 diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 48dca6b8..f5f651ba 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 183; - public static final String GIT_SHA = "8748a97ffc59d88be5af96f2815fed9bbf04f336"; - public static final String GIT_DATE = "2026-04-11 15:37:20 EDT"; + public static final int GIT_REVISION = 187; + public static final String GIT_SHA = "aebb9337cd5757c80fb19d8e3a9cdb4cd7d90f8a"; + public static final String GIT_DATE = "2026-06-22 17:47:54 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2026-04-15 16:57:36 EDT"; - public static final long BUILD_UNIX_TIME = 1776286656632L; + public static final String BUILD_DATE = "2026-06-25 18:38:47 EDT"; + public static final long BUILD_UNIX_TIME = 1782427127742L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index b6b939b2..42cd2b25 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -54,24 +54,29 @@ public void configureButtonBindings(Controller driver, Controller operator) { driver .createRightStickButton() .onTrue(Commands.runOnce(() -> drivetrain.toggleFieldOrientedDrive())); - driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); - driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); + // driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); + // driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); LEDStrip.setSegmentActive(FEEDER_LED, false); new Trigger(DriverStation::isTeleopEnabled) .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(FEEDER_LED, true))); driver .createAButton() .whileTrue( - lowerFlyWheel - .set(SPEED1) - .alongWith(upperFlyWheel.set(SPEED1 - UPPEROFFSET)) - .beforeStarting( + (lowerFlyWheel + .set(SPEED1) + .alongWith(upperFlyWheel.set(SPEED1 - UPPEROFFSET)) + .beforeStarting( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, true); + LEDStrip.changeSegmentPattern( + SHOOTER_LED, LEDStrip.getRainbowPattern(50)); + })) + .finallyDo( () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, true); - LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(50)); - }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); driver .createXButton() .whileTrue( @@ -83,7 +88,12 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.setSegmentActive(SHOOTER_LED, true); LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + .finallyDo( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); driver .createYButton() @@ -96,7 +106,12 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.setSegmentActive(SHOOTER_LED, true); LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + .finallyDo( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); driver .createBButton() .whileTrue( @@ -108,10 +123,44 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.setSegmentActive(SHOOTER_LED, true); LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(200)); }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - - driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); - driver.createStartButton().whileTrue(upperFlyWheel.systemID()); + .finallyDo( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); + + driver.A_BUTTON.onFalse( + Commands.runOnce( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); + driver.B_BUTTON.onFalse( + Commands.runOnce( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); + driver.X_BUTTON.onFalse( + Commands.runOnce( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); + driver.Y_BUTTON.onFalse( + Commands.runOnce( + () -> { + LEDStrip.setSegmentActive(SHOOTER_LED, false); + lowerFlyWheel.set(0); + upperFlyWheel.set(0); + })); + + // driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); + // driver.createStartButton().whileTrue(upperFlyWheel.systemID()); driver .createLeftBumper() .whileTrue( @@ -173,8 +222,10 @@ public void configureButtonBindings(Controller driver, Controller operator) { @Override public void setupDefaultCommands(Controller driver, Controller operator) { - lowerFlyWheel.setDefaultCommand(lowerFlyWheel.joyStickControl(() -> driver.getRightTrigger())); - upperFlyWheel.setDefaultCommand(upperFlyWheel.joyStickControl(() -> driver.getLeftTrigger())); + // lowerFlyWheel.setDefaultCommand(lowerFlyWheel.joyStickControl(() -> + // driver.getRightTrigger())); + // upperFlyWheel.setDefaultCommand(upperFlyWheel.joyStickControl(() -> + // driver.getLeftTrigger())); drivetrain.setDefaultCommand(drivetrain.createDefaultCommand(driver)); } diff --git a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java index 08b774a5..f3238389 100644 --- a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java @@ -237,7 +237,7 @@ public Command createDefaultCommand(Controller driver) { this, () -> driver.getLeftYAxis(), () -> driver.getLeftXAxis(), - () -> driver.getRightXAxis(), + () -> driver.getLeftTrigger(), () -> isFieldOrientedDrive.getValue()); } diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index b206e86a..167632f8 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -742,7 +742,7 @@ public Command driveWithSetpointGeneratorOrientationConsidered( public Command createDefaultCommand(Controller driverXbox) { DoubleSupplier leftX = () -> driverXbox.getLeftXAxis(); DoubleSupplier leftY = () -> driverXbox.getLeftYAxis(); - DoubleSupplier rightX = () -> driverXbox.getRightXAxis(); + DoubleSupplier rightX = () -> driverXbox.getLeftTrigger(); BooleanSupplier isFieldOriented = () -> isFieldOrientedDrive.getValue(); /** @@ -765,7 +765,7 @@ public Command createDefaultCommand(Controller driverXbox) { public Command createDefaultTestCommand(Controller driverXbox) { DoubleSupplier leftX = () -> driverXbox.getLeftXAxis(); DoubleSupplier leftY = () -> driverXbox.getLeftYAxis(); - DoubleSupplier rightX = () -> driverXbox.getRightXAxis(); + DoubleSupplier rightX = () -> driverXbox.getLeftTrigger(); BooleanSupplier isFieldOriented = () -> isFieldOrientedDrive.getValue(); driverXbox.createAButton().whileTrue(sysIdDriveMotorCommand()); From 3525ab0260c0e7d391ea780063b2ad781a9869a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 23:13:05 +0000 Subject: [PATCH 76/93] Fix A/B/X/Y flywheel buttons not stopping motors The flywheel stop logic called lowerFlyWheel.set(0)/upperFlyWheel.set(0) inside Runnable lambdas (finallyDo and Commands.runOnce). FlyWheel.set() returns a RunCommand that must be scheduled to take effect, so those returned commands were silently discarded and the motors were never commanded to zero. On button release the held set(SPEED) RunCommand is interrupted but does not zero its output, leaving the flywheels spinning. Bind the stop command to onFalse directly (matching the working feeder pattern), so the set(0) command is actually scheduled. Drop the dead set(0) calls from the whileTrue finallyDo blocks, leaving only the LED reset there. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../java/frc/robot/blackteam/BlackRobot.java | 68 ++++++------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 42cd2b25..e8e358c6 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -71,12 +71,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( SHOOTER_LED, LEDStrip.getRainbowPattern(50)); })) - .finallyDo( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver .createXButton() .whileTrue( @@ -88,12 +83,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.setSegmentActive(SHOOTER_LED, true); LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); }) - .finallyDo( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver .createYButton() @@ -106,12 +96,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.setSegmentActive(SHOOTER_LED, true); LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); }) - .finallyDo( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver .createBButton() .whileTrue( @@ -123,41 +108,28 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.setSegmentActive(SHOOTER_LED, true); LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(200)); }) - .finallyDo( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver.A_BUTTON.onFalse( - Commands.runOnce( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + lowerFlyWheel + .set(0) + .alongWith(upperFlyWheel.set(0)) + .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver.B_BUTTON.onFalse( - Commands.runOnce( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + lowerFlyWheel + .set(0) + .alongWith(upperFlyWheel.set(0)) + .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver.X_BUTTON.onFalse( - Commands.runOnce( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + lowerFlyWheel + .set(0) + .alongWith(upperFlyWheel.set(0)) + .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver.Y_BUTTON.onFalse( - Commands.runOnce( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, false); - lowerFlyWheel.set(0); - upperFlyWheel.set(0); - })); + lowerFlyWheel + .set(0) + .alongWith(upperFlyWheel.set(0)) + .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); // driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); // driver.createStartButton().whileTrue(upperFlyWheel.systemID()); From 820991240c98bef42efe135c494914731e83f124 Mon Sep 17 00:00:00 2001 From: Curt Date: Thu, 25 Jun 2026 21:40:04 -0400 Subject: [PATCH 77/93] Refactor driver button bindings and feeder directions Replace direct button constants with createXButton/createLeftBumper/createRightBumper/createStartButton bindings, consolidate multiple shooter stop onFalse handlers into a single binding, and invert feeder motor speeds for the corresponding button actions. These updates clarify controller mappings and ensure feeder/shooter commands and LED updates trigger and stop correctly. --- .../java/frc/robot/blackteam/BlackRobot.java | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index e8e358c6..faee0705 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -60,7 +60,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { new Trigger(DriverStation::isTeleopEnabled) .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(FEEDER_LED, true))); driver - .createAButton() + .createLeftBumper() .whileTrue( (lowerFlyWheel .set(SPEED1) @@ -98,7 +98,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { }) .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver - .createBButton() + .createRightBumper() .whileTrue( lowerFlyWheel .set(SPEED4) @@ -110,22 +110,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { }) .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver.A_BUTTON.onFalse( - lowerFlyWheel - .set(0) - .alongWith(upperFlyWheel.set(0)) - .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver.B_BUTTON.onFalse( - lowerFlyWheel - .set(0) - .alongWith(upperFlyWheel.set(0)) - .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver.X_BUTTON.onFalse( - lowerFlyWheel - .set(0) - .alongWith(upperFlyWheel.set(0)) - .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver.Y_BUTTON.onFalse( + driver.createBButton().onFalse( lowerFlyWheel .set(0) .alongWith(upperFlyWheel.set(0)) @@ -134,10 +119,10 @@ public void configureButtonBindings(Controller driver, Controller operator) { // driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); // driver.createStartButton().whileTrue(upperFlyWheel.systemID()); driver - .createLeftBumper() + .createAButton() .whileTrue( feeder - .setSpeed(-0.5) + .setSpeed(0.5) .beforeStarting( () -> { LEDStrip.setSegmentActive(FEEDER_LED, true); @@ -150,13 +135,13 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - driver.LEFT_BUMPER.onFalse(feeder.setSpeed(0)); + driver.A_BUTTON.onFalse(feeder.setSpeed(0)); driver - .createRightBumper() + .createStartButton() .whileTrue( feeder - .setSpeed(0.5) + .setSpeed(-0.5) .beforeStarting( () -> { LEDStrip.setSegmentActive(FEEDER_LED, true); @@ -169,7 +154,7 @@ public void configureButtonBindings(Controller driver, Controller operator) { LEDStrip.changeSegmentPattern( FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); - driver.RIGHT_BUMPER.onFalse(feeder.setSpeed(0)); + driver.START_BUTTON.onFalse(feeder.setSpeed(0)); State prep = flyWheelStateMachine.addState( From f065e21d5de9e345e8fd0fae371f77599406b5f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 01:52:35 +0000 Subject: [PATCH 78/93] Drive FeederSubsystem from JSON config like UpperFlyWheel Replace the hard-coded SmartMotorController/FlyWheel construction in FeederSubsystem with the config-driven GenericSubsystem pattern used by UpperFlyWheel: the constructor now calls super("feeder.json") and pulls the FlyWheel out of the devices map by name. Add the matching deploy configs (yams_shooter device) for both the black_robot and buttercup_swerve robot directories, reproducing the previous feeder tuning: Spark/Neo on CAN 12, 3x4 reduction, 40A stator limit, brake mode, 0.25s closed-loop ramp, profiled closed loop (p=0.00016541, 5000 rpm / 2500 rps^2), 3in diameter, 1 lb, +/-500 rpm soft limits. Verified in simulation: the feeder mechanism loads and telemeters under /Mechanisms/feeder alongside the flywheels, and robot startup completes with no parse errors. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../deploy/black_robot/subsystems/feeder.json | 8 +++ .../subsystems/feeder/flywheel.json | 68 +++++++++++++++++++ .../buttercup_swerve/subsystems/feeder.json | 8 +++ .../subsystems/feeder/flywheel.json | 68 +++++++++++++++++++ .../frc/robot/blackteam/FeederSubsystem.java | 59 +++------------- 5 files changed, 161 insertions(+), 50 deletions(-) create mode 100644 src/main/deploy/black_robot/subsystems/feeder.json create mode 100644 src/main/deploy/black_robot/subsystems/feeder/flywheel.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/feeder.json create mode 100644 src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json diff --git a/src/main/deploy/black_robot/subsystems/feeder.json b/src/main/deploy/black_robot/subsystems/feeder.json new file mode 100644 index 00000000..4a9c79da --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/feeder.json @@ -0,0 +1,8 @@ +{ + "devices": [ + { + "device": "yams_shooter", + "file": "feeder/flywheel.json" + } + ] +} diff --git a/src/main/deploy/black_robot/subsystems/feeder/flywheel.json b/src/main/deploy/black_robot/subsystems/feeder/flywheel.json new file mode 100644 index 00000000..b1932ef0 --- /dev/null +++ b/src/main/deploy/black_robot/subsystems/feeder/flywheel.json @@ -0,0 +1,68 @@ +{ + "motorSetup": { + "name": "feeder", + "logLevel": "HIGH", + "controllerType": "Spark", + "motorType": "Neo", + "canId": 12, + "inverted": false, + "idleMode": "BRAKE", + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "controlAlgorithm": "PROFILED", + "motorSystemId": { + "feedBack": { + "p": 0.00016541, + "i": 0.0, + "d": 0.0 + }, + "maxVelocity": { + "val": 5000, + "uom": "rpm" + }, + "maxAcceleration": { + "val": 2500, + "uom": "rps^2" + }, + "closedLoopRamp": { + "val": 0.25, + "uom": "s" + }, + "controlMode": "CLOSED_LOOP" + }, + "simSystemId": { + "feedBack": { + "p": 0.00016541, + "i": 0.0, + "d": 0.0 + }, + "maxVelocity": { + "val": 5000, + "uom": "rpm" + }, + "maxAcceleration": { + "val": 2500, + "uom": "rps^2" + } + }, + "lowerSoftLimit": { + "val": -500, + "uom": "rpm" + }, + "upperSoftLimit": { + "val": 500, + "uom": "rpm" + }, + "gearing": [3, 4], + "mass": { + "val": 1, + "uom": "lbs" + }, + "radius": { + "val": 1.5, + "uom": "in" + } +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/feeder.json b/src/main/deploy/buttercup_swerve/subsystems/feeder.json new file mode 100644 index 00000000..4a9c79da --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/feeder.json @@ -0,0 +1,8 @@ +{ + "devices": [ + { + "device": "yams_shooter", + "file": "feeder/flywheel.json" + } + ] +} diff --git a/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json new file mode 100644 index 00000000..b1932ef0 --- /dev/null +++ b/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json @@ -0,0 +1,68 @@ +{ + "motorSetup": { + "name": "feeder", + "logLevel": "HIGH", + "controllerType": "Spark", + "motorType": "Neo", + "canId": 12, + "inverted": false, + "idleMode": "BRAKE", + "currentLimit": { + "val": 40, + "uom": "amps" + } + }, + "controlAlgorithm": "PROFILED", + "motorSystemId": { + "feedBack": { + "p": 0.00016541, + "i": 0.0, + "d": 0.0 + }, + "maxVelocity": { + "val": 5000, + "uom": "rpm" + }, + "maxAcceleration": { + "val": 2500, + "uom": "rps^2" + }, + "closedLoopRamp": { + "val": 0.25, + "uom": "s" + }, + "controlMode": "CLOSED_LOOP" + }, + "simSystemId": { + "feedBack": { + "p": 0.00016541, + "i": 0.0, + "d": 0.0 + }, + "maxVelocity": { + "val": 5000, + "uom": "rpm" + }, + "maxAcceleration": { + "val": 2500, + "uom": "rps^2" + } + }, + "lowerSoftLimit": { + "val": -500, + "uom": "rpm" + }, + "upperSoftLimit": { + "val": 500, + "uom": "rpm" + }, + "gearing": [3, 4], + "mass": { + "val": 1, + "uom": "lbs" + }, + "radius": { + "val": 1.5, + "uom": "in" + } +} diff --git a/src/main/java/frc/robot/blackteam/FeederSubsystem.java b/src/main/java/frc/robot/blackteam/FeederSubsystem.java index 03337369..1e8d1ed2 100644 --- a/src/main/java/frc/robot/blackteam/FeederSubsystem.java +++ b/src/main/java/frc/robot/blackteam/FeederSubsystem.java @@ -4,64 +4,28 @@ package frc.robot.blackteam; -import static edu.wpi.first.units.Units.Amps; -import static edu.wpi.first.units.Units.Inches; -import static edu.wpi.first.units.Units.Pounds; -import static edu.wpi.first.units.Units.RPM; -import static edu.wpi.first.units.Units.RotationsPerSecondPerSecond; -import static edu.wpi.first.units.Units.Seconds; - -import com.revrobotics.spark.SparkLowLevel.MotorType; -import com.revrobotics.spark.SparkMax; -import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.wpilibj2.command.Command; import java.util.function.Supplier; import org.frc5010.common.arch.GenericSubsystem; -import yams.gearing.GearBox; -import yams.gearing.MechanismGearing; -import yams.mechanisms.config.FlyWheelConfig; import yams.mechanisms.velocity.FlyWheel; -import yams.motorcontrollers.SmartMotorController; -import yams.motorcontrollers.SmartMotorControllerConfig; -import yams.motorcontrollers.SmartMotorControllerConfig.ControlMode; -import yams.motorcontrollers.SmartMotorControllerConfig.MotorMode; -import yams.motorcontrollers.SmartMotorControllerConfig.TelemetryVerbosity; -import yams.motorcontrollers.local.SparkWrapper; /** Add your docs here. */ public class FeederSubsystem extends GenericSubsystem { - private final SparkMax motor = new SparkMax(12, MotorType.kBrushless); - private final SmartMotorControllerConfig motorConfig = - new SmartMotorControllerConfig(this) - .withClosedLoopController( - 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) - .withSimClosedLoopController( - 0.00016541, 0, 0, RPM.of(5000), RotationsPerSecondPerSecond.of(2500)) - .withGearing(new MechanismGearing(GearBox.fromReductionStages(3, 4))) - .withIdleMode(MotorMode.BRAKE) - .withTelemetry("FeederMotor", TelemetryVerbosity.HIGH) - .withStatorCurrentLimit(Amps.of(40)) - .withMotorInverted(false) - .withClosedLoopRampRate(Seconds.of(0.25)) - .withControlMode(ControlMode.CLOSED_LOOP); - - private final SmartMotorController motorController = - new SparkWrapper(motor, DCMotor.getNEO(1), motorConfig); + private FlyWheel feeder; - private final FlyWheelConfig feederConfig = - new FlyWheelConfig(motorController) - .withDiameter(Inches.of(3)) - .withMass(Pounds.of(1)) - .withTelemetry("FeederMech", TelemetryVerbosity.HIGH) - .withUpperSoftLimit(RPM.of(500)) - .withSpeedometerSimulation(); - private final FlyWheel feeder = new FlyWheel(feederConfig); + /** Creates a new Feeder configured from JSON. */ + public FeederSubsystem() { + super("feeder.json"); + feeder = (FlyWheel) devices.get("feeder"); + } public Command setSpeed(double speed) { return feeder.set(speed); } - public FeederSubsystem() {} + public Command joyStickControl(Supplier speedSupplier) { + return feeder.set(speedSupplier); + } @Override public void periodic() { @@ -72,9 +36,4 @@ public void periodic() { public void simulationPeriodic() { feeder.simIterate(); } - - public Command joyStickControl(Supplier speedSupplier) { - return feeder.set(speedSupplier); - } } -/** Robot, stop */ From 04b645791d4847de841d51718bee069ff6696eec Mon Sep 17 00:00:00 2001 From: Curt Date: Thu, 25 Jun 2026 21:56:09 -0400 Subject: [PATCH 79/93] Reverse feeder on butercup --- .../deploy/buttercup_swerve/subsystems/feeder/flywheel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json index b1932ef0..9cc88cf9 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json @@ -5,7 +5,7 @@ "controllerType": "Spark", "motorType": "Neo", "canId": 12, - "inverted": false, + "inverted": true, "idleMode": "BRAKE", "currentLimit": { "val": 40, From 27a487f7a46a98190f202f9ca1061f5e1fdaccae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 11:31:21 +0000 Subject: [PATCH 80/93] Feeder LED: green laser sweep, deactivate on release Replace the feeder LED behavior so it shows a sweeping green "laser" while feeding and returns to the alliance-color background when done. - Add LEDStrip.getLaserPattern(color, width, speed): a narrow colored band that scrolls along the segment over an unlit background. - Feeder A/Start buttons now light FEEDER_LED with the green laser on press and deactivate the segment on release (instead of leaving it solid red and active), so the left half reverts to alliance color. - Remove the teleop-enabled trigger that force-activated FEEDER_LED with its default unlit pattern, which blanked the left half of the strip during teleop (and leaked a perpetual command each enable). Drop the now-unused DriverStation, Trigger, and Seconds imports. Verified: compiles and the robot starts cleanly in simulation. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../java/frc/robot/blackteam/BlackRobot.java | 22 ++++--------------- .../frc5010/common/subsystems/LEDStrip.java | 19 ++++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index faee0705..2fee7e72 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -1,14 +1,11 @@ package frc.robot.blackteam; import static edu.wpi.first.units.Units.RPM; -import static edu.wpi.first.units.Units.Seconds; import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.util.Color; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Commands; -import edu.wpi.first.wpilibj2.command.button.Trigger; import org.frc5010.common.arch.GenericRobot; import org.frc5010.common.arch.StateMachine; import org.frc5010.common.arch.StateMachine.State; @@ -56,9 +53,6 @@ public void configureButtonBindings(Controller driver, Controller operator) { .onTrue(Commands.runOnce(() -> drivetrain.toggleFieldOrientedDrive())); // driver.setRightTrigger(driver.createRightTrigger().cubed().deadzone(0.05).scale(0.35)); // driver.setLeftTrigger(driver.createLeftTrigger().cubed().deadzone(0.05).scale(0.35)); - LEDStrip.setSegmentActive(FEEDER_LED, false); - new Trigger(DriverStation::isTeleopEnabled) - .onTrue(Commands.run(() -> LEDStrip.setSegmentActive(FEEDER_LED, true))); driver .createLeftBumper() .whileTrue( @@ -127,13 +121,9 @@ public void configureButtonBindings(Controller driver, Controller operator) { () -> { LEDStrip.setSegmentActive(FEEDER_LED, true); LEDStrip.changeSegmentPattern( - FEEDER_LED, - LEDStrip.getSolidPattern(Color.kGreen).blink(Seconds.of(.25))); + FEEDER_LED, LEDStrip.getLaserPattern(Color.kGreen, 0.2, 150)); }) - .finallyDo( - () -> - LEDStrip.changeSegmentPattern( - FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); + .finallyDo(() -> LEDStrip.setSegmentActive(FEEDER_LED, false))); driver.A_BUTTON.onFalse(feeder.setSpeed(0)); @@ -146,13 +136,9 @@ public void configureButtonBindings(Controller driver, Controller operator) { () -> { LEDStrip.setSegmentActive(FEEDER_LED, true); LEDStrip.changeSegmentPattern( - FEEDER_LED, - LEDStrip.getSolidPattern(Color.kGreen).blink(Seconds.of(.25))); + FEEDER_LED, LEDStrip.getLaserPattern(Color.kGreen, 0.2, 150)); }) - .finallyDo( - () -> - LEDStrip.changeSegmentPattern( - FEEDER_LED, LEDStrip.getSolidPattern(Color.kRed)))); + .finallyDo(() -> LEDStrip.setSegmentActive(FEEDER_LED, false))); driver.START_BUTTON.onFalse(feeder.setSpeed(0)); diff --git a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java index 1fce3083..f5cb321f 100644 --- a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java +++ b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java @@ -351,6 +351,25 @@ public static LEDPattern getSolidPattern(Color color) { return LEDPattern.solid(color); } + /** + * Returns a scrolling "laser" pattern: a narrow bright band of the given color that sweeps along + * the segment over an unlit (black) background. + * + * @param color the color of the laser band + * @param percentWidth the width of the laser band, as a fraction (0.0-1.0) of the segment's length + * @param percentScrollingSpeed the speed at which the band sweeps, as a percentage of the + * segment's length per second + * @return the new scrolling laser pattern + */ + public static LEDPattern getLaserPattern( + Color color, double percentWidth, double percentScrollingSpeed) { + Map maskSteps = Map.of(0.0, Color.kWhite, percentWidth, Color.kBlack); + LEDPattern mask = + LEDPattern.steps(maskSteps) + .scrollAtRelativeSpeed(Percent.per(Second).of(percentScrollingSpeed)); + return LEDPattern.solid(color).mask(mask); + } + /** * Returns a new LEDPattern that masks the given base pattern with a band of given visibility and * scrolling speed. The mask has a band of white (visible) that is percentVisible of the pattern's From ae6143258b1baf8f55e8f3d695a77fae29ae7ecb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 11:33:50 +0000 Subject: [PATCH 81/93] Fix getBand crash and addSegment end-index naming - getBand: the third mask step used bandCenter - percentWidth/2, which duplicated the second step's key (Map.of throws on duplicate keys) and was geometrically wrong. Use bandCenter + percentWidth/2 so the band's right edge is correct and the keys are distinct. - addSegment overloads: the range parameter was named "length" and documented as a length, but it is passed straight to AddressableLEDBuffer.createView(start, endIndex), whose second argument is an inclusive end index. Rename the parameter to endIndex and fix the Javadoc so the contract matches the (correct) behavior the config path already relies on. No behavior change to existing callers. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../frc5010/common/subsystems/LEDStrip.java | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java index f5cb321f..83121c79 100644 --- a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java +++ b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java @@ -227,55 +227,56 @@ public static void setSegmentActive(String name, boolean active) { } /** - * Adds a pattern to the LED strip with the given name, starting at the given index and spanning - * the given length. The pattern is active by default. If the name already corresponds to a - * pattern, this method does nothing. + * Adds a pattern to the LED strip with the given name, spanning the LEDs from the given start + * index to the given end index (inclusive). The pattern is active by default. If the name already + * corresponds to a pattern, this method does nothing. * * @param name The name of the pattern to add * @param pattern The pattern to add * @param startIndex The starting index of the pattern - * @param length The length of the pattern + * @param endIndex The ending index of the pattern (inclusive) */ - public static void addSegment(String name, LEDPattern pattern, int startIndex, int length) { + public static void addSegment(String name, LEDPattern pattern, int startIndex, int endIndex) { if (null == instance) { return; } - AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, length); + AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, endIndex); segments.put(name, Segment.create().setView(view).setPattern(pattern).setActive(true)); } /** - * Adds a pattern to the LED strip with the given name, starting at the given index and spanning - * the given length. If the name already corresponds to a pattern, this method does nothing. + * Adds a pattern to the LED strip with the given name, spanning the LEDs from the given start + * index to the given end index (inclusive). If the name already corresponds to a pattern, this + * method does nothing. * * @param name The name of the pattern to add * @param startIndex The starting index of the pattern - * @param length The length of the pattern + * @param endIndex The ending index of the pattern (inclusive) */ - public static void addSegment(String name, int startIndex, int length) { + public static void addSegment(String name, int startIndex, int endIndex) { if (null == instance) { return; } - AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, length); + AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, endIndex); segments.put(name, Segment.create().setView(view).setPattern(LEDPattern.kOff).setActive(false)); } /** - * Adds a pattern to the LED strip with the given name, starting at the given index and spanning - * the given length. The pattern is active by default. If the name already corresponds to a - * pattern, this method does nothing. + * Adds a pattern to the LED strip with the given name, spanning the LEDs from the given start + * index to the given end index (inclusive). The pattern is active by default. If the name already + * corresponds to a pattern, this method does nothing. * * @param name The name of the pattern to add * @param startIndex The starting index of the pattern - * @param length The length of the pattern + * @param endIndex The ending index of the pattern (inclusive) * @param order The order in which the pattern will be displayed. Lower order patterns will be * displayed before higher order patterns. */ - public static void addSegment(String name, int startIndex, int length, int order) { + public static void addSegment(String name, int startIndex, int endIndex, int order) { if (null == instance) { return; } - AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, length); + AddressableLEDBufferView view = instance.m_buffer.createView(startIndex, endIndex); segments.put( name, Segment.create() @@ -414,7 +415,7 @@ public static LEDPattern getBand( Color.kBlack, bandCenter - (percentWidth / 2), Color.kWhite, - bandCenter - (percentWidth / 2), + bandCenter + (percentWidth / 2), Color.kBlack); LEDPattern mask = LEDPattern.steps(maskSteps) From 7e13038964c1e01f574aa0096697f0339a9f4ce3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 11:57:03 +0000 Subject: [PATCH 82/93] Add swerve azimuth diagnostics logging and step-test command Instrument per-module steering so a wheel that fails to reach its commanded rotation can be diagnosed. - YAGSLSwerveDrivetrain.periodic() now logs Swerve/Diag/* every loop when the YAGSL telemetry verbosity is HIGH or above (same flag the rest of the swerve high-verbosity logging uses): desired vs measured azimuth and error, absolute-minus-relative encoder divergence, steer applied output, voltage and velocity, the absolute-encoder read-issue flag, a derived per-module "misaligned" flag, and the max absolute error. Desired angle comes from YAGSL's post-optimization commanded state. Logged through AdvantageKit so it lands in the .wpilog for replay, not just live NT. - Add SwerveDriveFunctions.pointModulesAt(degrees) (YAGSL override points every module's azimuth, drive motors untouched). - Add GenericSwerveDrivetrain.azimuthStepTestCommand(): a deterministic routine that steps all modules through a fixed angle sequence with a fixed dwell, logging Swerve/Diag/StepTest/targetDeg, for on-blocks diagnosis. Verified in simulation: all Swerve/Diag/* outputs are recorded with no periodic exceptions. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../drive/swerve/GenericSwerveDrivetrain.java | 31 +++++++ .../drive/swerve/SwerveDriveFunctions.java | 10 +++ .../drive/swerve/YAGSLSwerveDrivetrain.java | 86 +++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index 167632f8..94c41c3e 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -60,6 +60,7 @@ import org.frc5010.common.drive.swerve_utils.SwerveSetpointGenerator5010; import org.frc5010.common.sensors.Controller; import org.json.simple.parser.ParseException; +import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.mechanism.LoggedMechanism2d; import org.littletonrobotics.junction.mechanism.LoggedMechanismLigament2d; import org.littletonrobotics.junction.networktables.LoggedDashboardChooser; @@ -787,6 +788,36 @@ public Command sysIdAngleMotorCommand() { return swerveDrive.sysIdAngleMotorCommand(this); } + /** Azimuth angles (deg) the step test drives every module through, in order. */ + private static final double[] AZIMUTH_TEST_ANGLES = {0.0, 90.0, 180.0, -90.0, 45.0, 0.0}; + + /** How long (seconds) to hold each step so the modules can settle. */ + private static final double AZIMUTH_TEST_DWELL_SECONDS = 1.0; + + /** + * Deterministic azimuth diagnostic: points every module at a fixed sequence of angles, holding + * each for a fixed dwell, while the drive motors stay stopped. Run it with the robot on blocks and + * review {@code Swerve/Diag/*} (measured/error/steer effort) against {@code + * Swerve/Diag/StepTest/targetDeg} to see which module fails to reach its commanded rotation. The + * per-module diagnostics require the YAGSL telemetry verbosity to be HIGH. + * + * @return the step-test command (requires this drivetrain, interrupting the default drive command) + */ + public Command azimuthStepTestCommand() { + Command sequence = Commands.none(); + for (double angle : AZIMUTH_TEST_ANGLES) { + final double target = angle; + sequence = + sequence.andThen( + run(() -> { + Logger.recordOutput("Swerve/Diag/StepTest/targetDeg", target); + swerveDrive.pointModulesAt(target); + }) + .withTimeout(AZIMUTH_TEST_DWELL_SECONDS)); + } + return sequence.withName("AzimuthStepTest"); + } + public void resetEncoders() { swerveDrive.resetEncoders(); } diff --git a/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java b/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java index ba3ac82d..a73633ec 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java +++ b/src/main/java/org/frc5010/common/drive/swerve/SwerveDriveFunctions.java @@ -122,6 +122,16 @@ public void periodic() { // Default empty } + /** + * Commands every module's azimuth to the same absolute angle, in degrees. Default implementation + * does nothing; drivetrains that support direct azimuth control should override it. + * + * @param degrees the absolute steer angle to command, in degrees + */ + public void pointModulesAt(double degrees) { + // Default empty + } + public void updateSimulation() { // Default empty } diff --git a/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java index 960992a4..eb7575cf 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java @@ -13,6 +13,7 @@ import com.pathplanner.lib.commands.PathPlannerAuto; import com.pathplanner.lib.path.PathPlannerPath; import com.pathplanner.lib.util.DriveFeedforwards; +import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.controller.SimpleMotorFeedforward; import edu.wpi.first.math.geometry.Pose2d; @@ -44,6 +45,7 @@ import org.frc5010.common.constants.GenericDrivetrainConstants; import org.frc5010.common.drive.pose.DrivePoseEstimator; import org.frc5010.common.drive.pose.SwerveFunctionsPose; +import org.littletonrobotics.junction.Logger; import swervelib.SwerveController; import swervelib.SwerveDrive; import swervelib.SwerveDriveTest; @@ -612,6 +614,90 @@ public GenericSwerveModuleInfo[] getModulesInfo() { return moduleInfos; } + /** Azimuth error (deg) above which a settled module is flagged as misaligned. */ + private static final double AZIMUTH_ERROR_TOLERANCE_DEG = 5.0; + + /** Steer velocity (deg/s) below which a module is considered settled (not slewing). */ + private static final double AZIMUTH_SETTLED_VELOCITY_DPS = 10.0; + + @Override + public void periodic() { + logModuleDiagnostics(); + } + + /** + * Commands every module's azimuth to the same absolute angle. Intended for deterministic + * diagnostics (see {@code azimuthStepTestCommand}); the drive motors are left untouched. + * + * @param degrees the absolute steer angle to command, in degrees + */ + @Override + public void pointModulesAt(double degrees) { + for (SwerveModule module : swerveDrive.getModules()) { + module.setAngle(degrees); + } + } + + /** + * Logs per-module azimuth diagnostics under {@code Swerve/Diag/*} so desired-vs-actual rotation + * can be reviewed live in AdvantageScope and replayed from the log. Only runs when the YAGSL + * telemetry verbosity is HIGH or above, matching the rest of the swerve high-verbosity logging. + */ + private void logModuleDiagnostics() { + if (SwerveDriveTelemetry.verbosity.ordinal() < TelemetryVerbosity.HIGH.ordinal()) { + return; + } + SwerveModule[] modules = swerveDrive.getModules(); + SwerveModuleState[] desiredStates = SwerveDriveTelemetry.desiredStatesObj; + int count = modules.length; + double[] desiredDeg = new double[count]; + double[] measuredDeg = new double[count]; + double[] errorDeg = new double[count]; + double[] absMinusRelDeg = new double[count]; + double[] steerAppliedOutput = new double[count]; + double[] steerVoltage = new double[count]; + double[] steerVelocityDps = new double[count]; + boolean[] absoluteEncoderReadIssue = new boolean[count]; + boolean[] misaligned = new boolean[count]; + double maxAbsErrorDeg = 0.0; + for (int i = 0; i < count; i++) { + SwerveModule module = modules[i]; + double measured = module.getAbsolutePosition(); + double relative = module.getRelativePosition(); + // Use YAGSL's post-optimization commanded state as the true target; fall back to the + // measured state on the first loops before any command has been issued. + double desired = + (desiredStates != null && i < desiredStates.length && desiredStates[i] != null) + ? desiredStates[i].angle.getDegrees() + : module.getState().angle.getDegrees(); + double error = MathUtil.inputModulus(desired - measured, -180.0, 180.0); + double steerVel = module.getAngleMotor().getVelocity(); + + desiredDeg[i] = desired; + measuredDeg[i] = measured; + errorDeg[i] = error; + absMinusRelDeg[i] = MathUtil.inputModulus(measured - relative, -180.0, 180.0); + steerAppliedOutput[i] = module.getAngleMotor().getAppliedOutput(); + steerVoltage[i] = module.getAngleMotor().getVoltage(); + steerVelocityDps[i] = steerVel; + absoluteEncoderReadIssue[i] = module.getAbsoluteEncoderReadIssue(); + misaligned[i] = + Math.abs(error) > AZIMUTH_ERROR_TOLERANCE_DEG + && Math.abs(steerVel) < AZIMUTH_SETTLED_VELOCITY_DPS; + maxAbsErrorDeg = Math.max(maxAbsErrorDeg, Math.abs(error)); + } + Logger.recordOutput("Swerve/Diag/desiredAngleDeg", desiredDeg); + Logger.recordOutput("Swerve/Diag/measuredAngleDeg", measuredDeg); + Logger.recordOutput("Swerve/Diag/errorDeg", errorDeg); + Logger.recordOutput("Swerve/Diag/absMinusRelDeg", absMinusRelDeg); + Logger.recordOutput("Swerve/Diag/steerAppliedOutput", steerAppliedOutput); + Logger.recordOutput("Swerve/Diag/steerVoltage", steerVoltage); + Logger.recordOutput("Swerve/Diag/steerVelocityDps", steerVelocityDps); + Logger.recordOutput("Swerve/Diag/absoluteEncoderReadIssue", absoluteEncoderReadIssue); + Logger.recordOutput("Swerve/Diag/misaligned", misaligned); + Logger.recordOutput("Swerve/Diag/maxAbsErrorDeg", maxAbsErrorDeg); + } + @Override public void stop() { swerveDrive.drive(new ChassisSpeeds()); From bfd187f71a8290f686e68b280335cd3b0366c90f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 12:03:59 +0000 Subject: [PATCH 83/93] Bind azimuth step test in test mode; fix expected-steer dial - Expose azimuthStepTestCommand() on GenericDrivetrain (default no-op, overridden by GenericSwerveDrivetrain) and bind it to the driver A button in BlackRobot's test-mode bindings (configureAltButtonBindings), so the deterministic azimuth diagnostic can be run on blocks in Test. - Fix GenericSwerveModuleInfo.expectedSteerDegrees so the Glass "expected" dial shows the actual commanded (post-optimization) steer angle from YAGSL telemetry instead of the measured state angle. Falls back to the measured angle before any command has been issued. Verified: compiles and the robot starts and loops cleanly in simulation. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../java/frc/robot/blackteam/BlackRobot.java | 6 ++++++ .../common/drive/GenericDrivetrain.java | 12 +++++++++++ .../drive/swerve/GenericSwerveDrivetrain.java | 1 + .../drive/swerve/GenericSwerveModuleInfo.java | 20 ++++++++++++++++++- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 2fee7e72..edda8bde 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -163,6 +163,12 @@ public void configureButtonBindings(Controller driver, Controller operator) { // .alongWith(feeder.setSpeed(0))); } + @Override + public void configureAltButtonBindings(Controller driver, Controller operator) { + // Test-mode diagnostics: run the deterministic azimuth step test (robot on blocks). + driver.createAButton().onTrue(drivetrain.azimuthStepTestCommand()); + } + @Override public void setupDefaultCommands(Controller driver, Controller operator) { // lowerFlyWheel.setDefaultCommand(lowerFlyWheel.joyStickControl(() -> diff --git a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java index f3238389..20e78bef 100644 --- a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java @@ -28,6 +28,7 @@ import edu.wpi.first.wpilibj.smartdashboard.Field2d; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; +import edu.wpi.first.wpilibj2.command.Commands; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -257,6 +258,17 @@ public Command createDefaultTestCommand(Controller driver) { () -> isFieldOrientedDrive.getValue()); } + /** + * Deterministic azimuth diagnostic command. Drivetrains that support direct azimuth control + * (e.g. swerve) override this to step the modules through a fixed sequence of angles; the default + * implementation does nothing. + * + * @return the azimuth step-test command + */ + public Command azimuthStepTestCommand() { + return Commands.none(); + } + /** Resets the encoders */ public void resetEncoders() {} diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index 94c41c3e..e10ff32f 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -803,6 +803,7 @@ public Command sysIdAngleMotorCommand() { * * @return the step-test command (requires this drivetrain, interrupting the default drive command) */ + @Override public Command azimuthStepTestCommand() { Command sequence = Commands.none(); for (double angle : AZIMUTH_TEST_ANGLES) { diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveModuleInfo.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveModuleInfo.java index fde3b84c..9261283e 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveModuleInfo.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveModuleInfo.java @@ -4,8 +4,10 @@ package org.frc5010.common.drive.swerve; +import edu.wpi.first.math.kinematics.SwerveModuleState; import org.frc5010.common.drive.swerve.akit.Module; import swervelib.SwerveModule; +import swervelib.telemetry.SwerveDriveTelemetry; /** Add your docs here. */ public record GenericSwerveModuleInfo( @@ -23,7 +25,23 @@ public GenericSwerveModuleInfo(SwerveModule module) { module.getDriveMotor().getPosition(), module.getDriveMotor().getVelocity(), module.getAngleMotor().getVelocity(), - module.getState().angle.getDegrees()); + desiredSteerDegrees(module)); + } + + /** + * The module's commanded (desired) steer angle in degrees, taken from YAGSL's post-optimization + * telemetry. Falls back to the measured state angle before any command has been issued. + * + * @param module the swerve module + * @return the desired steer angle in degrees + */ + private static double desiredSteerDegrees(SwerveModule module) { + SwerveModuleState[] desired = SwerveDriveTelemetry.desiredStatesObj; + int index = module.moduleNumber; + if (desired != null && index >= 0 && index < desired.length && desired[index] != null) { + return desired[index].angle.getDegrees(); + } + return module.getState().angle.getDegrees(); } public GenericSwerveModuleInfo(Module module) { From 588bb2d6ab4b134dd5d8ebbd8f45821301f4f6e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 12:11:07 +0000 Subject: [PATCH 84/93] Add Azimuth Step Test dashboard button Register the deterministic azimuth diagnostic on SmartDashboard from the GenericSwerveDrivetrain constructor so it can be triggered from the dashboard (while enabled, robot on blocks) without entering Test mode, in addition to the Test-mode A-button binding. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- .../frc5010/common/drive/swerve/GenericSwerveDrivetrain.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index e10ff32f..0f094849 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -83,6 +83,10 @@ public GenericSwerveDrivetrain( setDrivetrainPoseEstimator(swerveDrive.initializePoseEstimator()); initializeSimulation(swerveConstants); driveTrainSimulationSupplier = swerveDrive.getDriveTrainSimulationSupplier(); + + // Expose the deterministic azimuth diagnostic as a dashboard button so it can be run + // (while enabled, robot on blocks) without entering Test mode. + SmartDashboard.putData("Azimuth Step Test", azimuthStepTestCommand()); } @Override From 8b7c4089b0743048a8166476171657b49aa41a94 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:57:13 +0000 Subject: [PATCH 85/93] Implement pointModulesAt for akit so the azimuth step test works SparkFishers' azimuth step test drives modules via SwerveDriveFunctions.pointModulesAt, which only YAGSL overrode, so it was a no-op on the akit drivetrains. Override it in AkitSwerveDrive: - Add Module.pointAt(Rotation2d): commands the steer position directly with the drive stopped, bypassing runSetpoint's nearest-equivalent optimize so the module physically rotates to the commanded angle (not a 180 deg flip) - which is what the diagnostic needs to measure. - AkitSwerveDrive.pointModulesAt points all modules and records the commanded angle in lastSetpointStates, so the always-on Drive/Diag/Module*/AngleErrorDeg reports target-vs-measured during the test. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NuiJsExocjmTcG2zKNJNYx --- .../drive/swerve/akit/AkitSwerveDrive.java | 16 ++++++++++++++++ .../frc5010/common/drive/swerve/akit/Module.java | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java index 5829ed74..75676e1c 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/AkitSwerveDrive.java @@ -280,6 +280,22 @@ public void stop() { runVelocity(new ChassisSpeeds()); } + /** + * Points every module's azimuth at the same absolute angle with the drive motors stopped. Backs + * the azimuth step test; also records the commanded angle as the module setpoint so the always-on + * Drive/Diag diagnostics report target-vs-measured error during the test. + * + * @param degrees the absolute steer angle to command, in degrees + */ + @Override + public void pointModulesAt(double degrees) { + Rotation2d target = Rotation2d.fromDegrees(degrees); + for (int i = 0; i < modules.length; i++) { + modules[i].pointAt(target); + lastSetpointStates[i] = new SwerveModuleState(0.0, target); + } + } + /** * Stops the drive and turns the modules to an X arrangement to resist movement. The modules will * return to their normal orientations the next time a nonzero velocity is requested. diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java b/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java index b911d5c1..c909bb75 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/Module.java @@ -104,6 +104,18 @@ public void runSteerCharacterization(double output) { io.setTurnOpenLoop(output); } + /** + * Points the module's azimuth at the given absolute angle with the drive motor stopped, without + * the nearest-equivalent optimization used by {@link #runSetpoint}. Used by the azimuth step test + * so the module physically rotates to the commanded angle (not an equivalent 180 deg flip). + * + * @param angle the absolute steer angle to hold + */ + public void pointAt(Rotation2d angle) { + io.setDriveOpenLoop(0.0); + io.setTurnPosition(angle); + } + /** Disables all outputs to motors. */ public void stop() { io.setDriveOpenLoop(0.0); From 34ce7ba35e883d4dde5284d29cf5db8625ec558f Mon Sep 17 00:00:00 2001 From: Curt Date: Sat, 27 Jun 2026 14:40:09 -0400 Subject: [PATCH 86/93] Updates to Elphaba CAN --- .../black_robot/subsystems/feeder/flywheel.json | 5 ++++- .../black_robot/yagsl_swerve/modules/backleft.json | 4 ++-- .../black_robot/yagsl_swerve/modules/backright.json | 4 ++-- .../black_robot/yagsl_swerve/modules/frontleft.json | 2 +- .../black_robot/yagsl_swerve/modules/frontright.json | 4 ++-- .../buttercup_swerve/subsystems/feeder/flywheel.json | 5 ++++- src/main/java/frc/robot/blackteam/BlackRobot.java | 12 +++++++----- .../org/frc5010/common/drive/GenericDrivetrain.java | 4 ++-- .../common/drive/swerve/GenericSwerveDrivetrain.java | 7 ++++--- .../java/org/frc5010/common/subsystems/LEDStrip.java | 3 ++- 10 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/main/deploy/black_robot/subsystems/feeder/flywheel.json b/src/main/deploy/black_robot/subsystems/feeder/flywheel.json index b1932ef0..8b4f2d07 100644 --- a/src/main/deploy/black_robot/subsystems/feeder/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/feeder/flywheel.json @@ -56,7 +56,10 @@ "val": 500, "uom": "rpm" }, - "gearing": [3, 4], + "gearing": [ + 3, + 4 + ], "mass": { "val": 1, "uom": "lbs" diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json index 560ed689..178cad4d 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backleft.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 3, + "id": 16, "canbus": null }, "angle": { "type": "neo", - "id": 4, + "id": 3, "canbus": null }, "encoder": { diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index 6afb711e..d196fee5 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 5, + "id": 4, "canbus": null }, "angle": { "type": "neo", - "id": 6, + "id": 15, "canbus": null }, "encoder": { diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json index df764070..7f0210c3 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontleft.json @@ -1,7 +1,7 @@ { "drive": { "type": "neo", - "id": 1, + "id": 18, "canbus": null }, "angle": { diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json index c578241b..85a5bf53 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/frontright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 7, + "id": 17, "canbus": null }, "angle": { "type": "neo", - "id": 8, + "id": 6, "canbus": null }, "encoder": { diff --git a/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json b/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json index 9cc88cf9..19f2f47d 100644 --- a/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json +++ b/src/main/deploy/buttercup_swerve/subsystems/feeder/flywheel.json @@ -56,7 +56,10 @@ "val": 500, "uom": "rpm" }, - "gearing": [3, 4], + "gearing": [ + 3, + 4 + ], "mass": { "val": 1, "uom": "lbs" diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index edda8bde..14b0549c 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -104,11 +104,13 @@ public void configureButtonBindings(Controller driver, Controller operator) { }) .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver.createBButton().onFalse( - lowerFlyWheel - .set(0) - .alongWith(upperFlyWheel.set(0)) - .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + driver + .createBButton() + .onFalse( + lowerFlyWheel + .set(0) + .alongWith(upperFlyWheel.set(0)) + .beforeStarting(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); // driver.createBackButton().whileTrue(lowerFlyWheel.systemID()); // driver.createStartButton().whileTrue(upperFlyWheel.systemID()); diff --git a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java index 20e78bef..2ddf9bb8 100644 --- a/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/GenericDrivetrain.java @@ -259,8 +259,8 @@ public Command createDefaultTestCommand(Controller driver) { } /** - * Deterministic azimuth diagnostic command. Drivetrains that support direct azimuth control - * (e.g. swerve) override this to step the modules through a fixed sequence of angles; the default + * Deterministic azimuth diagnostic command. Drivetrains that support direct azimuth control (e.g. + * swerve) override this to step the modules through a fixed sequence of angles; the default * implementation does nothing. * * @return the azimuth step-test command diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index 0f094849..47ada9db 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -800,12 +800,13 @@ public Command sysIdAngleMotorCommand() { /** * Deterministic azimuth diagnostic: points every module at a fixed sequence of angles, holding - * each for a fixed dwell, while the drive motors stay stopped. Run it with the robot on blocks and - * review {@code Swerve/Diag/*} (measured/error/steer effort) against {@code + * each for a fixed dwell, while the drive motors stay stopped. Run it with the robot on blocks + * and review {@code Swerve/Diag/*} (measured/error/steer effort) against {@code * Swerve/Diag/StepTest/targetDeg} to see which module fails to reach its commanded rotation. The * per-module diagnostics require the YAGSL telemetry verbosity to be HIGH. * - * @return the step-test command (requires this drivetrain, interrupting the default drive command) + * @return the step-test command (requires this drivetrain, interrupting the default drive + * command) */ @Override public Command azimuthStepTestCommand() { diff --git a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java index 83121c79..71bdbf40 100644 --- a/src/main/java/org/frc5010/common/subsystems/LEDStrip.java +++ b/src/main/java/org/frc5010/common/subsystems/LEDStrip.java @@ -357,7 +357,8 @@ public static LEDPattern getSolidPattern(Color color) { * the segment over an unlit (black) background. * * @param color the color of the laser band - * @param percentWidth the width of the laser band, as a fraction (0.0-1.0) of the segment's length + * @param percentWidth the width of the laser band, as a fraction (0.0-1.0) of the segment's + * length * @param percentScrollingSpeed the speed at which the band sweeps, as a percentage of the * segment's length per second * @return the new scrolling laser pattern From 62f55dfb64ef557f1fa5f7e9f6f7744f0414596e Mon Sep 17 00:00:00 2001 From: pkatzfey Date: Sat, 27 Jun 2026 14:54:48 -0500 Subject: [PATCH 87/93] Other updates --- .../subsystems/feeder/flywheel.json | 2 +- .../yagsl_swerve/modules/backright.json | 4 +- .../yagsl_swerve/modules/pidfproperties.json | 2 +- .../java/frc/robot/blackteam/BlackRobot.java | 79 ++++++++++--------- 4 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/main/deploy/black_robot/subsystems/feeder/flywheel.json b/src/main/deploy/black_robot/subsystems/feeder/flywheel.json index 8b4f2d07..19f2f47d 100644 --- a/src/main/deploy/black_robot/subsystems/feeder/flywheel.json +++ b/src/main/deploy/black_robot/subsystems/feeder/flywheel.json @@ -5,7 +5,7 @@ "controllerType": "Spark", "motorType": "Neo", "canId": 12, - "inverted": false, + "inverted": true, "idleMode": "BRAKE", "currentLimit": { "val": 40, diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json index d196fee5..04d8bc55 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/backright.json @@ -1,12 +1,12 @@ { "drive": { "type": "neo", - "id": 4, + "id": 15, "canbus": null }, "angle": { "type": "neo", - "id": 15, + "id": 4, "canbus": null }, "encoder": { diff --git a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json index bf95c1ca..2644bc19 100644 --- a/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json +++ b/src/main/deploy/black_robot/yagsl_swerve/modules/pidfproperties.json @@ -7,7 +7,7 @@ "iz": 0 }, "angle": { - "p": 0.01, + "p": 0.025, "i": 0, "d": 0, "f": 0.0, diff --git a/src/main/java/frc/robot/blackteam/BlackRobot.java b/src/main/java/frc/robot/blackteam/BlackRobot.java index 14b0549c..3cfefbcc 100644 --- a/src/main/java/frc/robot/blackteam/BlackRobot.java +++ b/src/main/java/frc/robot/blackteam/BlackRobot.java @@ -19,7 +19,7 @@ public class BlackRobot extends GenericRobot { private LowerFlyWheel lowerFlyWheel; private UpperFlyWheel upperFlyWheel; private StateMachine flyWheelStateMachine = new StateMachine(logPrefix); - private final double SPEED1 = 0.15; + private final double SPEED1 = 0.20; private final double SPEED2 = 0.35; private final double SPEED3 = 0.16; private final double SPEED4 = 0.17; @@ -66,43 +66,46 @@ public void configureButtonBindings(Controller driver, Controller operator) { SHOOTER_LED, LEDStrip.getRainbowPattern(50)); })) .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver - .createXButton() - .whileTrue( - lowerFlyWheel - .set(SPEED2) - .alongWith(upperFlyWheel.set(SPEED2 - UPPEROFFSET)) - .beforeStarting( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, true); - LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); - }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - - driver - .createYButton() - .whileTrue( - lowerFlyWheel - .set(SPEED3) - .alongWith(upperFlyWheel.set(SPEED3 - UPPEROFFSET)) - .beforeStarting( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, true); - LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(100)); - }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); - driver - .createRightBumper() - .whileTrue( - lowerFlyWheel - .set(SPEED4) - .alongWith(upperFlyWheel.set(SPEED4 - UPPEROFFSET)) - .beforeStarting( - () -> { - LEDStrip.setSegmentActive(SHOOTER_LED, true); - LEDStrip.changeSegmentPattern(SHOOTER_LED, LEDStrip.getRainbowPattern(200)); - }) - .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + // driver + // .createXButton() + // .whileTrue( + // lowerFlyWheel + // .set(SPEED2) + // .alongWith(upperFlyWheel.set(SPEED2 - UPPEROFFSET)) + // .beforeStarting( + // () -> { + // LEDStrip.setSegmentActive(SHOOTER_LED, true); + // LEDStrip.changeSegmentPattern(SHOOTER_LED, + // LEDStrip.getRainbowPattern(100)); + // }) + // .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + + // driver + // .createYButton() + // .whileTrue( + // lowerFlyWheel + // .set(SPEED3) + // .alongWith(upperFlyWheel.set(SPEED3 - UPPEROFFSET)) + // .beforeStarting( + // () -> { + // LEDStrip.setSegmentActive(SHOOTER_LED, true); + // LEDStrip.changeSegmentPattern(SHOOTER_LED, + // LEDStrip.getRainbowPattern(100)); + // }) + // .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); + // driver + // .createRightBumper() + // .whileTrue( + // lowerFlyWheel + // .set(SPEED4) + // .alongWith(upperFlyWheel.set(SPEED4 - UPPEROFFSET)) + // .beforeStarting( + // () -> { + // LEDStrip.setSegmentActive(SHOOTER_LED, true); + // LEDStrip.changeSegmentPattern(SHOOTER_LED, + // LEDStrip.getRainbowPattern(200)); + // }) + // .finallyDo(() -> LEDStrip.setSegmentActive(SHOOTER_LED, false))); driver .createBButton() From 8516b889ee3e72cbd3b573841f41095bf7850b86 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:33:55 +0000 Subject: [PATCH 88/93] Add swerve log analyzer for azimuth diagnostics tools/swerve_log_analysis.py reads an AdvantageKit .wpilog (stdlib only) and summarizes the Swerve/Diag/* signals: per-module desired/measured angle and error, absolute-vs-relative encoder gap, absolute-encoder read issues, and steer effort. It lists the timestamps where a module is flagged misaligned (settled but off target) with the concurrent signals so a miss can be mapped to encoder desync, a read failure, output saturation, or weak PID. Warns when a log was never enabled. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HmeQnHETe7J9KFoKXG56PM --- tools/swerve_log_analysis.py | 201 +++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100755 tools/swerve_log_analysis.py diff --git a/tools/swerve_log_analysis.py b/tools/swerve_log_analysis.py new file mode 100755 index 00000000..1774d9da --- /dev/null +++ b/tools/swerve_log_analysis.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Swerve azimuth diagnostics from an AdvantageKit WPILOG. + +Reads a ``.wpilog`` (no external dependencies) and summarizes the +``Swerve/Diag/*`` signals added in GenericSwerveDrivetrain/YAGSLSwerveDrivetrain, +so a wheel that fails to reach its commanded rotation can be diagnosed offline. + +Usage: + python3 tools/swerve_log_analysis.py [--error-deg 5] + +For each per-module signal it reports min/max/range; it also lists the moments +where a module is flagged ``misaligned`` (settled but off target) and prints the +concurrent signals (absolute-vs-relative encoder gap, absolute-encoder read +issue, steer applied output / voltage / velocity) so the failure can be mapped +to a cause: + * abs-vs-rel gap grows -> relative/absolute encoder desync + * absoluteEncoderReadIssue true -> intermittent CAN read failure + * steer output railed, vel ~0 -> saturation / mechanical stall + * small steady error, ~0 output -> weak angle PID + +The signals only exist in logs from the instrumented build (the commit that +added ``Swerve/Diag/*``). For older logs, fall back to YAGSL's ``swerve/`` +desired/measured states in AdvantageScope. +""" + +import bisect +import struct +import sys + +DIAG = "/RealOutputs/Swerve/Diag/" + + +def _read_str(buf, off): + n = struct.unpack_from("(name,type) and name->[(ts_us, value), ...].""" + buf = open(path, "rb").read() + if buf[:6] != b"WPILOG": + raise ValueError("not a WPILOG file") + extra_len = struct.unpack_from("> 2) & 0x3) + 1 + ts_len = ((head >> 4) & 0x7) + 1 + if off + id_len + size_len + ts_len > n: + break + eid = int.from_bytes(buf[off : off + id_len], "little") + off += id_len + psize = int.from_bytes(buf[off : off + size_len], "little") + off += size_len + ts = int.from_bytes(buf[off : off + ts_len], "little") + off += ts_len + payload = buf[off : off + psize] + off += psize + if eid == 0: # control record + if not payload: + continue + if payload[0] == 0: # Start + start_id = struct.unpack_from("= 0 else series[0][1] + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + path = sys.argv[1] + error_deg = 5.0 + if "--error-deg" in sys.argv: + error_deg = float(sys.argv[sys.argv.index("--error-deg") + 1]) + + _, data = parse(path) + all_ts = [t for v in data.values() for t, _ in v] + if not all_ts: + print("No records decoded.") + return + t0, t1 = min(all_ts), max(all_ts) + print("file: %s" % path) + print("duration: %.1f s entries: %d" % ((t1 - t0) / 1e6, len(data))) + + enabled = data.get("/DriverStation/Enabled", []) + if enabled: + frac = sum(1 for _, x in enabled if x) / len(enabled) + print("enabled fraction: %.2f (%d transitions)" % (frac, len(enabled))) + if frac == 0: + print("WARNING: robot never enabled in this log - no driving data.") + + diag = {k: v for k, v in data.items() if k.startswith(DIAG)} + if not diag: + print( + "\nNo Swerve/Diag/* signals - log predates the instrumented build.\n" + "Use YAGSL's swerve/ desired vs measured states in AdvantageScope instead." + ) + return + + print("\n--- per-signal ranges ---") + for name in sorted(diag): + short = name[len(DIAG):] + vals = [x for _, x in diag[name] if x is not None] + if not vals: + print("%-26s (no data)" % short) + continue + if isinstance(vals[0], list): + cols = max(len(a) for a in vals) + spans = [] + for c in range(cols): + col = [a[c] for a in vals if len(a) > c] + spans.append("[% .1f,% .1f]" % (min(col), max(col))) + print("%-26s %s" % (short, " ".join(spans))) + elif isinstance(vals[0], bool): + print("%-26s true=%d / %d samples" % (short, sum(vals), len(vals))) + else: + print("%-26s min=% .3f max=% .3f" % (short, min(vals), max(vals))) + + mis = data.get(DIAG + "misaligned", []) + err = data.get(DIAG + "errorDeg", []) + absrel = data.get(DIAG + "absMinusRelDeg", []) + readissue = data.get(DIAG + "absoluteEncoderReadIssue", []) + out = data.get(DIAG + "steerAppliedOutput", []) + vel = data.get(DIAG + "steerVelocityDps", []) + + print("\n--- misalignment events (settled but off target) ---") + events = 0 + for ts, flags in mis: + if not flags or not any(flags): + continue + events += 1 + rel = (ts - t0) / 1e6 + bad = [i for i, f in enumerate(flags) if f] + e = value_at(err, ts) or [] + ar = value_at(absrel, ts) or [] + ri = value_at(readissue, ts) or [] + ao = value_at(out, ts) or [] + sv = value_at(vel, ts) or [] + + def g(arr, i): + return arr[i] if i < len(arr) else float("nan") + + print("t=%7.2fs modules %s" % (rel, bad)) + for i in bad: + print( + " mod%d: err=% 7.1f absMinusRel=% 7.1f readIssue=%s " + "out=% .2f vel=% .1f" + % (i, g(e, i), g(ar, i), bool(g(ri, i)), g(ao, i), g(sv, i)) + ) + if events >= 40: + print(" ... (more events truncated)") + break + if events == 0: + print("none flagged (no module exceeded %.0f deg while settled)" % error_deg) + + +if __name__ == "__main__": + main() From 0695d8a09f38d3238f9b8c432556b288e6bb890f Mon Sep 17 00:00:00 2001 From: Curt Date: Mon, 29 Jun 2026 18:47:26 -0400 Subject: [PATCH 89/93] Use YAGSL drive and improve pointModulesAt Switch the robot deploy config to the YAGSL swerve drive. Implement deterministic per-move steering in YAGSLSwerveDrivetrain.pointModulesAt by tracking the last target, synchronizing module encoders on a new target, and issuing a zero-drive setDesiredState for a fresh, re-seeded command; repeated calls hold angle via setAngle. Bump BuildConstants (git revision/sha/branch/dates/build time). Include minor whitespace/javadoc formatting tweaks in ModuleIO/ModuleIOSpark. --- src/main/deploy/buttercup_swerve/robot.json | 2 +- src/main/java/frc/robot/BuildConstants.java | 12 +++---- .../drive/swerve/YAGSLSwerveDrivetrain.java | 32 +++++++++++++++++-- .../common/drive/swerve/akit/ModuleIO.java | 1 + .../drive/swerve/akit/ModuleIOSpark.java | 10 +++--- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/main/deploy/buttercup_swerve/robot.json b/src/main/deploy/buttercup_swerve/robot.json index 5973e5f7..75cbef10 100644 --- a/src/main/deploy/buttercup_swerve/robot.json +++ b/src/main/deploy/buttercup_swerve/robot.json @@ -1,6 +1,6 @@ { "userConfig": "demo_mode.json", - "driveType": "AKIT_SWERVE_DRIVE", + "driveType": "YAGSL_SWERVE_DRIVE", "trackWidth": 18.5625, "trackWidthUom": "in", "wheelBase": 18.5625, diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index f5f651ba..54c667a5 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 187; - public static final String GIT_SHA = "aebb9337cd5757c80fb19d8e3a9cdb4cd7d90f8a"; - public static final String GIT_DATE = "2026-06-22 17:47:54 EDT"; - public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2026-06-25 18:38:47 EDT"; - public static final long BUILD_UNIX_TIME = 1782427127742L; + public static final int GIT_REVISION = 215; + public static final String GIT_SHA = "ecd57e0a3c2c049364be08f65a6d3087e2f411b4"; + public static final String GIT_DATE = "2026-06-27 15:57:53 EDT"; + public static final String GIT_BRANCH = "claude/buttercup-akit-config"; + public static final String BUILD_DATE = "2026-06-29 18:38:28 EDT"; + public static final long BUILD_UNIX_TIME = 1782772708984L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java index eb7575cf..f63ddaa3 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/YAGSLSwerveDrivetrain.java @@ -625,16 +625,42 @@ public void periodic() { logModuleDiagnostics(); } + /** Last azimuth commanded by {@link #pointModulesAt(double)}; NaN until the first command. */ + private double lastPointModulesTarget = Double.NaN; + /** * Commands every module's azimuth to the same absolute angle. Intended for deterministic - * diagnostics (see {@code azimuthStepTestCommand}); the drive motors are left untouched. + * diagnostics (see {@code azimuthStepTestCommand}); the drive motors are left stopped. + * + *

At the start of each new target this queues {@link + * SwerveDrive#synchronizeModuleEncoders()} and commands through {@link + * SwerveModule#setDesiredState(SwerveModuleState, boolean, double)}, which consumes the queued + * sync and re-seeds the internal (motor) encoder from the absolute encoder before commanding — + * mirroring how YAGSL corrects internal-encoder drift "in small doses" while stopped. The + * drive-feedforward overload is used so no azimuth optimization or anti-jitter is applied (the + * commanded angle stays exact) and the drive motor is held at zero. While dwelling on the same + * target it holds with {@link SwerveModule#setAngle(double)} (internal encoder only, no further + * sync), so the logged {@code Swerve/Diag/errorDeg} reflects the drift accrued during a single + * move from a freshly-synced start. That isolates per-move steering error from the cumulative + * drift seen when sync is bypassed entirely (a healthy gear train tracks within ~1 deg; a module + * with slip/backlash/wrong ratio shows the per-move error directly). * * @param degrees the absolute steer angle to command, in degrees */ @Override public void pointModulesAt(double degrees) { - for (SwerveModule module : swerveDrive.getModules()) { - module.setAngle(degrees); + boolean newStep = degrees != lastPointModulesTarget; + lastPointModulesTarget = degrees; + if (newStep) { + swerveDrive.synchronizeModuleEncoders(); + SwerveModuleState target = new SwerveModuleState(0.0, Rotation2d.fromDegrees(degrees)); + for (SwerveModule module : swerveDrive.getModules()) { + module.setDesiredState(target, true, 0.0); + } + } else { + for (SwerveModule module : swerveDrive.getModules()) { + module.setAngle(degrees); + } } } diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java index 3ea71de1..8970e42f 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIO.java @@ -26,6 +26,7 @@ class ModuleIOInputs { public Rotation2d turnAbsolutePosition = Rotation2d.kZero; /** Absolute encoder reading before the configured zero offset is applied (diagnostics). */ public Rotation2d turnRawAbsolutePosition = Rotation2d.kZero; + public double turnVelocityRadPerSec = 0.0; public double turnAppliedVolts = 0.0; public double turnCurrentAmps = 0.0; diff --git a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java index aa572330..0571775b 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java +++ b/src/main/java/org/frc5010/common/drive/swerve/akit/ModuleIOSpark.java @@ -45,12 +45,12 @@ * Module IO implementation for a NEO (Spark Max) drive motor, NEO (Spark Max) turn motor, and an * absolute encoder (e.g. Canandmag) read through the turn Spark's absolute-encoder port. * - *

Everything per-robot is read from the deploy JSON (via {@link AkitSwerveConfig} / - * {@link SwerveModuleConstants}): drive/steer CAN IDs, drive gear ratio, drive current limit, the + *

Everything per-robot is read from the deploy JSON (via {@link AkitSwerveConfig} / {@link + * SwerveModuleConstants}): drive/steer CAN IDs, drive gear ratio, drive current limit, the * motor/encoder inversions, the absolute encoder zero offset, and the drive/steer closed-loop gains - * ({@code driveMotorControl}/{@code steerMotorControl} in akit units). Only universal akit constants - * (encoder conversion factors, the steer wrap range, steer current limit, odometry frequency) come - * from {@link DriveConstants}. + * ({@code driveMotorControl}/{@code steerMotorControl} in akit units). Only universal akit + * constants (encoder conversion factors, the steer wrap range, steer current limit, odometry + * frequency) come from {@link DriveConstants}. */ public class ModuleIOSpark implements ModuleIO { /** Absolute encoder zero offset, from the JSON {@code absoluteOffset}. */ From 137f19cef7acb02170a61d7d8edfbad3565e75f6 Mon Sep 17 00:00:00 2001 From: Curt Date: Thu, 23 Jul 2026 17:42:27 -0400 Subject: [PATCH 90/93] Swerve diagnostic --- src/main/java/frc/robot/BuildConstants.java | 10 +- .../SteeringRatioCharacterization.java | 383 ++++++++++++++++++ .../drive/swerve/GenericSwerveDrivetrain.java | 26 ++ 3 files changed, 414 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/frc5010/common/commands/calibration/SteeringRatioCharacterization.java diff --git a/src/main/java/frc/robot/BuildConstants.java b/src/main/java/frc/robot/BuildConstants.java index 48dca6b8..567f15a2 100644 --- a/src/main/java/frc/robot/BuildConstants.java +++ b/src/main/java/frc/robot/BuildConstants.java @@ -5,12 +5,12 @@ public final class BuildConstants { public static final String MAVEN_GROUP = ""; public static final String MAVEN_NAME = "TeamBlack2025"; public static final String VERSION = "unspecified"; - public static final int GIT_REVISION = 183; - public static final String GIT_SHA = "8748a97ffc59d88be5af96f2815fed9bbf04f336"; - public static final String GIT_DATE = "2026-04-11 15:37:20 EDT"; + public static final int GIT_REVISION = 187; + public static final String GIT_SHA = "aebb9337cd5757c80fb19d8e3a9cdb4cd7d90f8a"; + public static final String GIT_DATE = "2026-06-22 17:47:54 EDT"; public static final String GIT_BRANCH = "LobbinLoco"; - public static final String BUILD_DATE = "2026-04-15 16:57:36 EDT"; - public static final long BUILD_UNIX_TIME = 1776286656632L; + public static final String BUILD_DATE = "2026-07-23 17:32:11 EDT"; + public static final long BUILD_UNIX_TIME = 1784842331905L; public static final int DIRTY = 1; private BuildConstants() {} diff --git a/src/main/java/org/frc5010/common/commands/calibration/SteeringRatioCharacterization.java b/src/main/java/org/frc5010/common/commands/calibration/SteeringRatioCharacterization.java new file mode 100644 index 00000000..fc57b48b --- /dev/null +++ b/src/main/java/org/frc5010/common/commands/calibration/SteeringRatioCharacterization.java @@ -0,0 +1,383 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.frc5010.common.commands.calibration; + +import static edu.wpi.first.units.Units.Seconds; + +import edu.wpi.first.math.MathUtil; +import edu.wpi.first.math.kinematics.ChassisSpeeds; +import edu.wpi.first.units.measure.Time; +import edu.wpi.first.wpilibj.Timer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.frc5010.common.arch.GenericCommand; +import org.frc5010.common.drive.swerve.GenericSwerveDrivetrain; +import org.frc5010.common.drive.swerve.GenericSwerveModuleInfo; +import org.frc5010.common.telemetry.DisplayBoolean; +import org.frc5010.common.telemetry.DisplayDouble; +import org.frc5010.common.telemetry.DisplayString; +import org.frc5010.common.telemetry.DisplayValuesHelper; + +/** + * Diagnoses steering (azimuth) modules that do not rotate the commanded amount. + * + *

The command walks the commanded module azimuth around in small, equal steps (default 45° + * per step, 32 steps, i.e. four full azimuth revolutions). Because every step is smaller than + * 90°, {@link edu.wpi.first.math.kinematics.SwerveModuleState#optimize} never flips a module, + * so every module should physically rotate exactly the commanded amount, continuously, in the same + * direction. + * + *

At the end of each step three angles are sampled per module and accumulated as wrapped deltas: + * + *

    + *
  • commanded — the azimuth this command asked for + *
  • absolute — {@link GenericSwerveModuleInfo#steerAbsoluteDegrees()}, the + * CANcoder / duty-cycle encoder on the azimuth output, i.e. true shaft rotation + *
  • relative — {@link GenericSwerveModuleInfo#steerRelativeDegrees()}, the steer + * motor's internal encoder scaled by the configured angle conversion factor + *
+ * + * Two ratios fall out, both of which are 1.0 on a healthy module: + * + *
    + *
  • Rotation Ratio = absolute / commanded. Less than 1.0 means the azimuth physically + * moved less than it was told to — a slipping pulley/belt, a stripped gear, or a loose + * encoder magnet. This is the check that catches a module rotating at a different rate than + * its siblings. + *
  • Encoder Ratio = relative / absolute. Not 1.0 means the steer motor turned a + * different number of rotations than the azimuth actually moved, i.e. the configured angle + * conversion factor (gear ratio) does not match the hardware, or something is slipping + * between the motor and the azimuth ring. Multiply the configured angle conversion factor by + * the reported Suggested Conv Scale (= absolute / relative) to correct it. + *
+ * + *

Each module's Rotation Ratio is also compared against the median of all modules, so a single + * bad module is called out by name even when the absolute numbers look plausible. + * + *

Run this with the robot up on blocks. A small drive velocity is required so the + * drivetrain actually commands azimuth (YAGSL holds the last angle when the requested speed is near + * zero), which means the wheels will spin and the robot will drive itself in a spiral if it is on + * the floor. + * + *

Note: on the AdvantageKit drivetrain the absolute and relative angles come from the same + * signal, so Encoder Ratio is always exactly 1.0 there and only Rotation Ratio is meaningful. + */ +public class SteeringRatioCharacterization extends GenericCommand { + /** + * Default azimuth change per step, in degrees. Must stay below 90 to avoid state optimization. + */ + public static final double DEFAULT_STEP_DEGREES = 45.0; + + /** Default number of steps, 32 * 45 degrees = four full azimuth revolutions. */ + public static final int DEFAULT_STEPS = 32; + + /** Default fraction of a full turn a ratio may deviate before being called a fault. */ + public static final double DEFAULT_RATIO_TOLERANCE = 0.05; + + private final DisplayValuesHelper displayValuesHelper = + new DisplayValuesHelper(logPrefix, "Steering Ratio"); + + private final GenericSwerveDrivetrain drivetrain; + + private final double stepDegrees; + private final int totalSteps; + private final double ratioTolerance; + private final double driveSpeedMetersPerSecond; + + private final Time alignTime = Seconds.of(1.5); + private final Time holdTime = Seconds.of(0.5); + private final Time finalSettleTime = Seconds.of(1.5); + + // Test sequence, built in init() + private final List setpointDegrees = new ArrayList<>(); + private final List setpointHoldSeconds = new ArrayList<>(); + private int setpointIndex = 0; + + private final Timer holdTimer = new Timer(); + private boolean finished = false; + + // Per-module accumulators + private double[] lastAbsoluteDegrees; + private double[] lastRelativeDegrees; + private double[] cumulativeAbsoluteDegrees; + private double[] cumulativeRelativeDegrees; + private double[] worstStepErrorDegrees; + private double[] worstFollowErrorDegrees; + private double cumulativeCommandedDegrees = 0.0; + + // Telemetry + private DisplayDouble[] rotationRatioDisplays; + private DisplayDouble[] encoderRatioDisplays; + private DisplayDouble[] conversionScaleDisplays; + private DisplayDouble[] worstStepErrorDisplays; + private DisplayDouble[] worstFollowErrorDisplays; + private DisplayDouble commandedTotalDisplay; + private DisplayDouble progressDisplay; + private DisplayBoolean faultDetectedDisplay; + private DisplayString resultDisplay; + + /** + * Creates a steering ratio characterization with the default sweep. + * + * @param drivetrain the swerve drivetrain to test + */ + public SteeringRatioCharacterization(GenericSwerveDrivetrain drivetrain) { + this(drivetrain, DEFAULT_STEP_DEGREES, DEFAULT_STEPS, DEFAULT_RATIO_TOLERANCE, 0.25); + } + + /** + * Creates a steering ratio characterization. + * + * @param drivetrain the swerve drivetrain to test + * @param stepDegrees azimuth change per step; must be less than 90 so module state optimization + * never flips a module mid-sweep + * @param totalSteps how many steps to take; more steps means more accumulated rotation and a + * tighter ratio estimate + * @param ratioTolerance how far a ratio may sit from 1.0, or from the median of the modules, + * before it is reported as a fault + * @param driveSpeedMetersPerSecond drive velocity requested while stepping; must be large enough + * that the drivetrain actually commands azimuth rather than holding the last angle + */ + public SteeringRatioCharacterization( + GenericSwerveDrivetrain drivetrain, + double stepDegrees, + int totalSteps, + double ratioTolerance, + double driveSpeedMetersPerSecond) { + this.drivetrain = drivetrain; + this.stepDegrees = stepDegrees; + this.totalSteps = totalSteps; + this.ratioTolerance = ratioTolerance; + this.driveSpeedMetersPerSecond = driveSpeedMetersPerSecond; + + if (Math.abs(stepDegrees) >= 90.0) { + throw new IllegalArgumentException( + "stepDegrees must be less than 90 so module optimization does not flip a module"); + } + + int moduleCount = drivetrain.getModulesInfo().length; + rotationRatioDisplays = new DisplayDouble[moduleCount]; + encoderRatioDisplays = new DisplayDouble[moduleCount]; + conversionScaleDisplays = new DisplayDouble[moduleCount]; + worstStepErrorDisplays = new DisplayDouble[moduleCount]; + worstFollowErrorDisplays = new DisplayDouble[moduleCount]; + for (int i = 0; i < moduleCount; i++) { + rotationRatioDisplays[i] = displayValuesHelper.makeDisplayDouble("M" + i + " Rotation Ratio"); + encoderRatioDisplays[i] = displayValuesHelper.makeDisplayDouble("M" + i + " Encoder Ratio"); + conversionScaleDisplays[i] = + displayValuesHelper.makeDisplayDouble("M" + i + " Suggested Conv Scale"); + worstStepErrorDisplays[i] = + displayValuesHelper.makeDisplayDouble("M" + i + " Worst Step Error (deg)"); + worstFollowErrorDisplays[i] = + displayValuesHelper.makeDisplayDouble("M" + i + " Worst Follow Error (deg)"); + } + commandedTotalDisplay = displayValuesHelper.makeDisplayDouble("Commanded Total (deg)"); + progressDisplay = displayValuesHelper.makeDisplayDouble("Progress (%)"); + faultDetectedDisplay = displayValuesHelper.makeDisplayBoolean("Fault Detected"); + resultDisplay = displayValuesHelper.makeDisplayString("Result"); + + addRequirements(drivetrain); + } + + @Override + public void init() { + int moduleCount = drivetrain.getModulesInfo().length; + lastAbsoluteDegrees = new double[moduleCount]; + lastRelativeDegrees = new double[moduleCount]; + cumulativeAbsoluteDegrees = new double[moduleCount]; + cumulativeRelativeDegrees = new double[moduleCount]; + worstStepErrorDegrees = new double[moduleCount]; + worstFollowErrorDegrees = new double[moduleCount]; + cumulativeCommandedDegrees = 0.0; + + setpointDegrees.clear(); + setpointHoldSeconds.clear(); + // Align to zero first; the sample at the end of this hold is the baseline. + setpointDegrees.add(0.0); + setpointHoldSeconds.add(alignTime.in(Seconds)); + for (int step = 1; step <= totalSteps; step++) { + setpointDegrees.add(step * stepDegrees); + setpointHoldSeconds.add(holdTime.in(Seconds)); + } + // Repeat the last setpoint with a long settle so the final sample is not polluted by + // steering lag; only the final position matters, intermediate lag cancels out. + setpointDegrees.add(totalSteps * stepDegrees); + setpointHoldSeconds.add(finalSettleTime.in(Seconds)); + + setpointIndex = 0; + finished = false; + + faultDetectedDisplay.setValue(false); + resultDisplay.setValue("Running - keep the robot on blocks"); + progressDisplay.setValue(0.0); + + holdTimer.reset(); + holdTimer.start(); + } + + @Override + public void execute() { + double commanded = setpointDegrees.get(setpointIndex); + commandAzimuth(commanded); + + if (!holdTimer.hasElapsed(setpointHoldSeconds.get(setpointIndex))) { + return; + } + + sample(commanded, setpointIndex > 0); + + setpointIndex++; + progressDisplay.setValue(100.0 * setpointIndex / setpointDegrees.size()); + if (setpointIndex >= setpointDegrees.size()) { + report(); + finished = true; + return; + } + holdTimer.reset(); + } + + /** Points every module at the given azimuth by requesting a robot-relative translation. */ + private void commandAzimuth(double azimuthDegrees) { + double radians = Math.toRadians(azimuthDegrees); + drivetrain.drive( + new ChassisSpeeds( + driveSpeedMetersPerSecond * Math.cos(radians), + driveSpeedMetersPerSecond * Math.sin(radians), + 0.0)); + } + + /** + * Records the current module angles. + * + * @param commandedDegrees the azimuth that was being held + * @param accumulate false for the baseline sample, which only seeds the last-angle arrays + */ + private void sample(double commandedDegrees, boolean accumulate) { + GenericSwerveModuleInfo[] modules = drivetrain.getModulesInfo(); + double commandedStep = accumulate ? wrapHalf(commandedDegrees - lastCommandedDegrees()) : 0.0; + + for (int i = 0; i < modules.length; i++) { + double absolute = modules[i].steerAbsoluteDegrees(); + double relative = modules[i].steerRelativeDegrees(); + + if (accumulate) { + double absoluteStep = wrapHalf(absolute - lastAbsoluteDegrees[i]); + double relativeStep = wrapHalf(relative - lastRelativeDegrees[i]); + cumulativeAbsoluteDegrees[i] += absoluteStep; + cumulativeRelativeDegrees[i] += relativeStep; + worstStepErrorDegrees[i] = + Math.max(worstStepErrorDegrees[i], Math.abs(absoluteStep - commandedStep)); + worstFollowErrorDegrees[i] = + Math.max(worstFollowErrorDegrees[i], Math.abs(wrapHalf(absolute - commandedDegrees))); + } + + lastAbsoluteDegrees[i] = absolute; + lastRelativeDegrees[i] = relative; + } + + if (accumulate) { + cumulativeCommandedDegrees += commandedStep; + commandedTotalDisplay.setValue(cumulativeCommandedDegrees); + } + } + + /** The setpoint held during the previous sample. */ + private double lastCommandedDegrees() { + return setpointDegrees.get(Math.max(0, setpointIndex - 1)); + } + + /** Computes the ratios, publishes them, and writes a human readable verdict. */ + private void report() { + int moduleCount = cumulativeAbsoluteDegrees.length; + double[] rotationRatios = new double[moduleCount]; + double[] encoderRatios = new double[moduleCount]; + + for (int i = 0; i < moduleCount; i++) { + rotationRatios[i] = + cumulativeCommandedDegrees == 0.0 + ? Double.NaN + : cumulativeAbsoluteDegrees[i] / cumulativeCommandedDegrees; + encoderRatios[i] = + cumulativeAbsoluteDegrees[i] == 0.0 + ? Double.NaN + : cumulativeRelativeDegrees[i] / cumulativeAbsoluteDegrees[i]; + + rotationRatioDisplays[i].setValue(rotationRatios[i]); + encoderRatioDisplays[i].setValue(encoderRatios[i]); + conversionScaleDisplays[i].setValue(1.0 / encoderRatios[i]); + worstStepErrorDisplays[i].setValue(worstStepErrorDegrees[i]); + worstFollowErrorDisplays[i].setValue(worstFollowErrorDegrees[i]); + } + + double medianRotationRatio = median(rotationRatios); + StringBuilder verdict = new StringBuilder(); + boolean fault = false; + + for (int i = 0; i < moduleCount; i++) { + List problems = new ArrayList<>(); + if (Double.isNaN(rotationRatios[i]) || Math.abs(rotationRatios[i] - 1.0) > ratioTolerance) { + problems.add(String.format("rotates %.1f%% of commanded", 100.0 * rotationRatios[i])); + } + if (Math.abs(rotationRatios[i] - medianRotationRatio) > ratioTolerance) { + problems.add( + String.format( + "%.1f%% off the other modules", 100.0 * (rotationRatios[i] - medianRotationRatio))); + } + if (Double.isNaN(encoderRatios[i]) || Math.abs(encoderRatios[i] - 1.0) > ratioTolerance) { + problems.add( + String.format( + "gearing conversion off by %.1f%% (scale conv factor by %.4f)", + 100.0 * (encoderRatios[i] - 1.0), 1.0 / encoderRatios[i])); + } + if (!problems.isEmpty()) { + fault = true; + verdict + .append("M") + .append(i) + .append(": ") + .append(String.join("; ", problems)) + .append(" | "); + } + } + + if (!fault) { + verdict.append( + String.format( + "All modules within %.0f%% over %.0f deg commanded", + 100.0 * ratioTolerance, cumulativeCommandedDegrees)); + } + + faultDetectedDisplay.setValue(fault); + resultDisplay.setValue(verdict.toString()); + log(logPrefix + ": " + verdict); + } + + /** Wraps a degree delta into (-90, 90], the largest range a single step can legally span. */ + private static double wrapHalf(double degrees) { + return MathUtil.inputModulus(degrees, -90.0, 90.0); + } + + private static double median(double[] values) { + double[] sorted = values.clone(); + Arrays.sort(sorted); + int middle = sorted.length / 2; + return sorted.length % 2 == 0 ? (sorted[middle - 1] + sorted[middle]) / 2.0 : sorted[middle]; + } + + @Override + public void stop(boolean interrupted) { + drivetrain.stop(); + holdTimer.stop(); + if (interrupted) { + resultDisplay.setValue("Interrupted before completing the sweep"); + } + } + + @Override + public boolean isFinished() { + return finished; + } +} diff --git a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java index b206e86a..4b22639e 100644 --- a/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java +++ b/src/main/java/org/frc5010/common/drive/swerve/GenericSwerveDrivetrain.java @@ -52,6 +52,7 @@ import org.frc5010.common.auto.pathplanner.PathFinderCommand; import org.frc5010.common.commands.DriveToPoseSupplier; import org.frc5010.common.commands.JoystickToSwerve; +import org.frc5010.common.commands.calibration.SteeringRatioCharacterization; import org.frc5010.common.constants.GenericDrivetrainConstants; import org.frc5010.common.constants.RobotConstantsDef; import org.frc5010.common.drive.GenericDrivetrain; @@ -167,6 +168,16 @@ public GenericDrivetrainConstants getSwerveConstants() { return swerveConstants; } + /** + * Gets per-module steering and drive readings, including both the absolute and the relative + * (motor internal) azimuth angles. + * + * @return one {@link GenericSwerveModuleInfo} per module + */ + public GenericSwerveModuleInfo[] getModulesInfo() { + return swerveDrive.getModulesInfo(); + } + @Override protected ChassisSpeeds getChassisSpeeds() { return swerveDrive.getRobotVelocity(); @@ -769,6 +780,8 @@ public Command createDefaultTestCommand(Controller driverXbox) { BooleanSupplier isFieldOriented = () -> isFieldOrientedDrive.getValue(); driverXbox.createAButton().whileTrue(sysIdDriveMotorCommand()); + driverXbox.createBButton().whileTrue(steeringRatioCharacterizationCommand()); + SmartDashboard.putData("Steering Ratio Test", steeringRatioCharacterizationCommand()); // driverXbox.createBButton().whileTrue(sysIdAngleMotorCommand()); // return Commands.run(() -> SwerveDriveTest.centerModules(swerveDrive), this); return new JoystickToSwerve( @@ -787,6 +800,19 @@ public Command sysIdAngleMotorCommand() { return swerveDrive.sysIdAngleMotorCommand(this); } + /** + * Sweeps the module azimuths in equal steps and reports, per module, how much they actually + * rotated versus how much they were told to. Catches a module whose steering gearing is slipping + * or whose configured angle conversion factor does not match the hardware. + * + *

Run with the robot up on blocks; the wheels spin during the test. + * + * @return the characterization {@link Command} + */ + public Command steeringRatioCharacterizationCommand() { + return new SteeringRatioCharacterization(this); + } + public void resetEncoders() { swerveDrive.resetEncoders(); } From bababaf4c24200f6b29dcff9c7316d6a78478eab Mon Sep 17 00:00:00 2001 From: Curt Date: Thu, 23 Jul 2026 17:54:10 -0400 Subject: [PATCH 91/93] Switch back to akit --- src/main/deploy/buttercup_swerve/robot.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/deploy/buttercup_swerve/robot.json b/src/main/deploy/buttercup_swerve/robot.json index 75cbef10..5973e5f7 100644 --- a/src/main/deploy/buttercup_swerve/robot.json +++ b/src/main/deploy/buttercup_swerve/robot.json @@ -1,6 +1,6 @@ { "userConfig": "demo_mode.json", - "driveType": "YAGSL_SWERVE_DRIVE", + "driveType": "AKIT_SWERVE_DRIVE", "trackWidth": 18.5625, "trackWidthUom": "in", "wheelBase": 18.5625, From 73cccb5f5cbae01d06b4e886085e869b72489f98 Mon Sep 17 00:00:00 2001 From: Curt Date: Mon, 3 Aug 2026 20:57:53 -0400 Subject: [PATCH 92/93] Update libraries --- .../controllers/axis/driver_left_x.json | 1 - vendordeps/AdvantageKit.json | 6 ++--- vendordeps/REVLib.json | 18 +++++++------- ...b-2026.1.1.json => ReduxLib-2026.1.2.json} | 12 +++++----- ...Lib-2026.1.0.json => ThriftyLib-2026.json} | 6 ++--- vendordeps/photonlib.json | 12 +++++----- vendordeps/yams.json | 24 ++++++++++++++++--- 7 files changed, 48 insertions(+), 31 deletions(-) rename vendordeps/{ReduxLib-2026.1.1.json => ReduxLib-2026.1.2.json} (89%) rename vendordeps/{ThriftyLib-2026.1.0.json => ThriftyLib-2026.json} (81%) diff --git a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json index d77f0e91..41719d6a 100644 --- a/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json +++ b/src/main/deploy/buttercup_swerve/controllers/axis/driver_left_x.json @@ -3,6 +3,5 @@ "deadband": 0.07, "invert": true, "scale": 1.0, - "curvePower": 3, "limit": 1.0 } diff --git a/vendordeps/AdvantageKit.json b/vendordeps/AdvantageKit.json index 91b4e342..469ae542 100644 --- a/vendordeps/AdvantageKit.json +++ b/vendordeps/AdvantageKit.json @@ -1,7 +1,7 @@ { "fileName": "AdvantageKit.json", "name": "AdvantageKit", - "version": "26.0.1", + "version": "26.0.2", "uuid": "d820cc26-74e3-11ec-90d6-0242ac120003", "frcYear": "2026", "mavenUrls": [ @@ -12,14 +12,14 @@ { "groupId": "org.littletonrobotics.akit", "artifactId": "akit-java", - "version": "26.0.1" + "version": "26.0.2" } ], "jniDependencies": [ { "groupId": "org.littletonrobotics.akit", "artifactId": "akit-wpilibio", - "version": "26.0.1", + "version": "26.0.2", "skipInvalidPlatforms": false, "isJar": false, "validPlatforms": [ diff --git a/vendordeps/REVLib.json b/vendordeps/REVLib.json index 1d80ce77..e7a0655f 100644 --- a/vendordeps/REVLib.json +++ b/vendordeps/REVLib.json @@ -1,7 +1,7 @@ { "fileName": "REVLib.json", "name": "REVLib", - "version": "2026.0.3", + "version": "2026.0.5", "frcYear": "2026", "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", "mavenUrls": [ @@ -12,14 +12,14 @@ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-java", - "version": "2026.0.3" + "version": "2026.0.5" } ], "jniDependencies": [ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-driver", - "version": "2026.0.3", + "version": "2026.0.5", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -34,7 +34,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "RevLibBackendDriver", - "version": "2026.0.3", + "version": "2026.0.5", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -49,7 +49,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "RevLibWpiBackendDriver", - "version": "2026.0.3", + "version": "2026.0.5", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -66,7 +66,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-cpp", - "version": "2026.0.3", + "version": "2026.0.5", "libName": "REVLib", "headerClassifier": "headers", "sharedLibrary": false, @@ -83,7 +83,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "REVLib-driver", - "version": "2026.0.3", + "version": "2026.0.5", "libName": "REVLibDriver", "headerClassifier": "headers", "sharedLibrary": false, @@ -100,7 +100,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "RevLibBackendDriver", - "version": "2026.0.3", + "version": "2026.0.5", "libName": "BackendDriver", "sharedLibrary": true, "skipInvalidPlatforms": true, @@ -116,7 +116,7 @@ { "groupId": "com.revrobotics.frc", "artifactId": "RevLibWpiBackendDriver", - "version": "2026.0.3", + "version": "2026.0.5", "libName": "REVLibWpi", "sharedLibrary": true, "skipInvalidPlatforms": true, diff --git a/vendordeps/ReduxLib-2026.1.1.json b/vendordeps/ReduxLib-2026.1.2.json similarity index 89% rename from vendordeps/ReduxLib-2026.1.1.json rename to vendordeps/ReduxLib-2026.1.2.json index 9081f8b2..431fe17e 100644 --- a/vendordeps/ReduxLib-2026.1.1.json +++ b/vendordeps/ReduxLib-2026.1.2.json @@ -1,7 +1,7 @@ { - "fileName": "ReduxLib-2026.1.1.json", + "fileName": "ReduxLib-2026.1.2.json", "name": "ReduxLib", - "version": "2026.1.1", + "version": "2026.1.2", "frcYear": "2026", "uuid": "151ecca8-670b-4026-8160-cdd2679ef2bd", "mavenUrls": [ @@ -12,14 +12,14 @@ { "groupId": "com.reduxrobotics.frc", "artifactId": "ReduxLib-java", - "version": "2026.1.1" + "version": "2026.1.2" } ], "jniDependencies": [ { "groupId": "com.reduxrobotics.frc", "artifactId": "ReduxLib-fifo", - "version": "2026.1.1", + "version": "2026.1.2", "isJar": false, "skipInvalidPlatforms": true, "validPlatforms": [ @@ -35,7 +35,7 @@ { "groupId": "com.reduxrobotics.frc", "artifactId": "ReduxLib-cpp", - "version": "2026.1.1", + "version": "2026.1.2", "libName": "ReduxLib", "headerClassifier": "headers", "sourcesClassifier": "sources", @@ -52,7 +52,7 @@ { "groupId": "com.reduxrobotics.frc", "artifactId": "ReduxLib-fifo", - "version": "2026.1.1", + "version": "2026.1.2", "libName": "reduxfifo", "headerClassifier": "headers", "sharedLibrary": true, diff --git a/vendordeps/ThriftyLib-2026.1.0.json b/vendordeps/ThriftyLib-2026.json similarity index 81% rename from vendordeps/ThriftyLib-2026.1.0.json rename to vendordeps/ThriftyLib-2026.json index ec3338c5..69917f06 100644 --- a/vendordeps/ThriftyLib-2026.1.0.json +++ b/vendordeps/ThriftyLib-2026.json @@ -1,7 +1,7 @@ { - "fileName": "ThriftyLib-2026.1.0.json", + "fileName": "ThriftyLib-2026.json", "name": "ThriftyLib", - "version": "2026.1.0", + "version": "2026.1.2", "frcYear": "2026", "uuid": "60b2694b-9e6e-4026-81ee-6f167946f4b0", "mavenUrls": [ @@ -12,7 +12,7 @@ { "groupId": "com.thethriftybot.frc", "artifactId": "ThriftyLib-java", - "version": "2026.1.0" + "version": "2026.1.2" } ], "jniDependencies": [], diff --git a/vendordeps/photonlib.json b/vendordeps/photonlib.json index 62661dd2..4280265f 100644 --- a/vendordeps/photonlib.json +++ b/vendordeps/photonlib.json @@ -1,7 +1,7 @@ { "fileName": "photonlib.json", "name": "photonlib", - "version": "v2026.3.1", + "version": "v2026.3.4", "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004", "frcYear": "2026", "mavenUrls": [ @@ -13,7 +13,7 @@ { "groupId": "org.photonvision", "artifactId": "photontargeting-cpp", - "version": "v2026.3.1", + "version": "v2026.3.4", "skipInvalidPlatforms": true, "isJar": false, "validPlatforms": [ @@ -28,7 +28,7 @@ { "groupId": "org.photonvision", "artifactId": "photonlib-cpp", - "version": "v2026.3.1", + "version": "v2026.3.4", "libName": "photonlib", "headerClassifier": "headers", "sharedLibrary": true, @@ -43,7 +43,7 @@ { "groupId": "org.photonvision", "artifactId": "photontargeting-cpp", - "version": "v2026.3.1", + "version": "v2026.3.4", "libName": "photontargeting", "headerClassifier": "headers", "sharedLibrary": true, @@ -60,12 +60,12 @@ { "groupId": "org.photonvision", "artifactId": "photonlib-java", - "version": "v2026.3.1" + "version": "v2026.3.4" }, { "groupId": "org.photonvision", "artifactId": "photontargeting-java", - "version": "v2026.3.1" + "version": "v2026.3.4" } ] } diff --git a/vendordeps/yams.json b/vendordeps/yams.json index 597f32f1..2d48cf86 100644 --- a/vendordeps/yams.json +++ b/vendordeps/yams.json @@ -1,7 +1,7 @@ { "fileName": "yams.json", "name": "Yet Another Mechanism System", - "version": "2026.2.24", + "version": "2026.7.10", "frcYear": "2026", "uuid": "a1051e86-a979-4880-a28b-a0d5362d1d96", "mavenUrls": [ @@ -12,10 +12,28 @@ { "groupId": "yams", "artifactId": "YAMS-java", - "version": "2026.2.24" + "version": "2026.7.10" + } + ], + "cppDependencies": [ + { + "groupId": "yams", + "artifactId": "YAMS-cpp", + "version": "2026.7.10", + "libName": "YAMS", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] } ], - "cppDependencies": [], "jniDependencies": [], "requires": [] } From 0ec07c238997b3b9a168705286c3e309432ec0e1 Mon Sep 17 00:00:00 2001 From: Curt Date: Mon, 3 Aug 2026 21:01:09 -0400 Subject: [PATCH 93/93] Switch YAMS back --- vendordeps/yams.json | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/vendordeps/yams.json b/vendordeps/yams.json index 2d48cf86..034dc6a8 100644 --- a/vendordeps/yams.json +++ b/vendordeps/yams.json @@ -1,7 +1,7 @@ { "fileName": "yams.json", "name": "Yet Another Mechanism System", - "version": "2026.7.10", + "version": "2026.2.23", "frcYear": "2026", "uuid": "a1051e86-a979-4880-a28b-a0d5362d1d96", "mavenUrls": [ @@ -12,28 +12,10 @@ { "groupId": "yams", "artifactId": "YAMS-java", - "version": "2026.7.10" - } - ], - "cppDependencies": [ - { - "groupId": "yams", - "artifactId": "YAMS-cpp", - "version": "2026.7.10", - "libName": "YAMS", - "headerClassifier": "headers", - "sharedLibrary": true, - "skipInvalidPlatforms": true, - "binaryPlatforms": [ - "windowsx86-64", - "linuxarm64", - "linuxx86-64", - "linuxathena", - "linuxarm32", - "osxuniversal" - ] + "version": "2026.2.23" } ], + "cppDependencies": [], "jniDependencies": [], "requires": [] }

Credits to - * p2reneker25 for building this. - */ -public class DIODutyCycleEncoderSwerve extends SwerveAbsoluteEncoder { - - /** Duty Cycle Encoder. */ - private final DutyCycleEncoder encoder; - /** Inversion state. */ - private boolean isInverted; - /** An {@link Alert} for if the encoder cannot report accurate velocities. */ - private Alert inaccurateVelocities; - /** The Offset in degrees of the DIO absolute encoder. */ - private double offset; - - /** - * Constructor for the DIO duty cycle encoder. - * - * @param pin DIO lane for the encoder. - */ - public DIODutyCycleEncoderSwerve(int pin) { - encoder = new DutyCycleEncoder(pin); - Timer.delay(2); - inaccurateVelocities = - new Alert( - "Encoders", - "The DIO Duty Cycle encoder may not report accurate velocities!", - AlertType.kWarning); - } - - @Override - public void close() { - encoder.close(); - } - - /** - * Configure the inversion state of the encoder. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - isInverted = inverted; - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - return (isInverted ? -1.0 : 1.0) * ((encoder.get() * 360) - offset); - } - - /** - * Get the encoder object. - * - * @return {@link DutyCycleEncoder} from the class. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - inaccurateVelocities.set(true); - return encoder.get(); - } - - /** Reset the encoder to factory defaults. */ - @Override - public void factoryDefault() { - // Do nothing - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - // Do nothing - } - - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - this.offset = offset; - - return true; - } -} diff --git a/src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java b/src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java deleted file mode 100644 index e36159cc..00000000 --- a/src/main/java/swervelib/encoders/SparkFlexEncoderSwerve.java +++ /dev/null @@ -1,136 +0,0 @@ -package swervelib.encoders; - -import com.revrobotics.AbsoluteEncoder; -import com.revrobotics.spark.SparkAbsoluteEncoder; -import com.revrobotics.spark.SparkFlex; -import com.revrobotics.spark.config.SparkFlexConfig; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import swervelib.motors.SparkFlexSwerve; -import swervelib.motors.SwerveMotor; - -/** SparkFlex absolute encoder, attached through the data port. */ -public class SparkFlexEncoderSwerve extends SwerveAbsoluteEncoder { - - /** The {@link AbsoluteEncoder} representing the duty cycle encoder attached to the SparkFlex. */ - public SparkAbsoluteEncoder encoder; - /** An {@link Alert} for if there is a failure configuring the encoder. */ - private Alert failureConfiguring; - /** {@link SparkFlexSwerve} instance. */ - private SwerveMotor sparkFlex; - - /** - * Create the {@link SparkFlexEncoderSwerve} object as a duty cycle from the {@link SparkFlex} - * motor. - * - * @param motor Motor to create the encoder from. - * @param conversionFactor The conversion factor to set if the output is not from 0 to 360. - */ - public SparkFlexEncoderSwerve(SwerveMotor motor, int conversionFactor) { - failureConfiguring = - new Alert("Encoders", "Failure configuring SparkFlex Absolute Encoder", AlertType.kWarning); - if (motor.getMotor() instanceof SparkFlex) { - sparkFlex = motor; - encoder = ((SparkFlex) motor.getMotor()).getAbsoluteEncoder(); - setConversionFactor(conversionFactor); - } else { - throw new RuntimeException( - "Motor given to instantiate SparkFlexEncoder is not a CANSparkFlex"); - } - } - - @Override - public void close() { - // SPARK Flex encoder gets closed with the motor - // I don't think an encoder getting closed should - // close the entire motor so i will keep this empty - // sparkFlex.close(); - } - - /** Reset the encoder to factory defaults. */ - @Override - public void factoryDefault() { - // Do nothing - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - // Do nothing - } - - /** - * Configure the absolute encoder to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - if (sparkFlex instanceof SparkFlexSwerve) { - SparkFlexConfig cfg = ((SparkFlexSwerve) sparkFlex).getConfig(); - cfg.absoluteEncoder.inverted(inverted); - ((SparkFlexSwerve) sparkFlex).updateConfig(cfg); - } - } - - /** - * Set the conversion factor of the {@link SparkFlexEncoderSwerve}. - * - * @param conversionFactor Position conversion factor from ticks to unit. - */ - public void setConversionFactor(double conversionFactor) { - SparkFlexConfig cfg = ((SparkFlexSwerve) sparkFlex).getConfig(); - cfg.signals.absoluteEncoderPositionAlwaysOn(true).absoluteEncoderPositionPeriodMs(20); - cfg.absoluteEncoder - .positionConversionFactor(conversionFactor) - .velocityConversionFactor(conversionFactor / 60); - ((SparkFlexSwerve) sparkFlex).updateConfig(cfg); - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - return encoder.getPosition(); - } - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Sets the Absolute Encoder Offset inside of the SparkFlex's Memory. - * - * @param offset the offset the Absolute Encoder uses as the zero point. - * @return if setting Absolute Encoder Offset was successful or not. - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - if (sparkFlex instanceof SparkFlexSwerve) { - SparkFlexConfig cfg = ((SparkFlexSwerve) sparkFlex).getConfig(); - cfg.absoluteEncoder.zeroOffset(offset); - ((SparkFlexSwerve) sparkFlex).updateConfig(cfg); - return true; - } - return false; - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - return encoder.getVelocity(); - } -} diff --git a/src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java b/src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java deleted file mode 100644 index cbb4a7ad..00000000 --- a/src/main/java/swervelib/encoders/SparkMaxAnalogEncoderSwerve.java +++ /dev/null @@ -1,185 +0,0 @@ -package swervelib.encoders; - -import com.revrobotics.REVLibError; -import com.revrobotics.spark.SparkAnalogSensor; -import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.ClosedLoopConfig.FeedbackSensor; -import com.revrobotics.spark.config.SparkMaxConfig; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import java.util.function.Supplier; -import swervelib.motors.SparkMaxBrushedMotorSwerve; -import swervelib.motors.SparkMaxSwerve; -import swervelib.motors.SwerveMotor; - -/** SparkMax absolute encoder, attached through the data port analog pin. */ -public class SparkMaxAnalogEncoderSwerve extends SwerveAbsoluteEncoder { - - /** - * {@link swervelib.motors.SparkMaxSwerve} or {@link swervelib.motors.SparkMaxBrushedMotorSwerve} - * object. - */ - private final SwerveMotor sparkMax; - /** - * The {@link SparkAnalogSensor} representing the duty cycle encoder attached to the SparkMax - * analog port. - */ - public SparkAnalogSensor encoder; - /** An {@link Alert} for if there is a failure configuring the encoder. */ - private Alert failureConfiguring; - /** An {@link Alert} for if the absolute encoder does not support integrated offsets. */ - private Alert doesNotSupportIntegratedOffsets; - - /** - * Create the {@link SparkMaxAnalogEncoderSwerve} object as a analog sensor from the {@link - * SparkMax} motor data port analog pin. - * - * @param motor Motor to create the encoder from. - * @param maxVoltage Maximum voltage for analog input reading. - */ - public SparkMaxAnalogEncoderSwerve(SwerveMotor motor, double maxVoltage) { - if (motor.getMotor() instanceof SparkMax) { - sparkMax = motor; - encoder = ((SparkMax) motor.getMotor()).getAnalog(); - setConversionFactor(360.0 / maxVoltage); - } else { - throw new RuntimeException("Motor given to instantiate SparkMaxEncoder is not a CANSparkMax"); - } - failureConfiguring = - new Alert("Encoders", "Failure configuring SparkMax Analog Encoder", AlertType.kWarning); - doesNotSupportIntegratedOffsets = - new Alert( - "Encoders", - "SparkMax Analog Sensors do not support integrated offsets", - AlertType.kWarning); - } - - @Override - public void close() { - // SPARK MAX Analog encoder gets closed with the motor - // I don't think an encoder getting closed should - // close the entire motor so i will keep this empty - // sparkMax.close(); - } - - /** - * Run the configuration until it succeeds or times out. - * - * @param config Lambda supplier returning the error state. - */ - private void configureSparkMax(Supplier config) { - for (int i = 0; i < maximumRetries; i++) { - if (config.get() == REVLibError.kOk) { - return; - } - } - failureConfiguring.set(true); - } - - /** - * Set the conversion factor of the {@link SparkMaxAnalogEncoderSwerve}. - * - * @param conversionFactor Position conversion factor from ticks to unit. - */ - public void setConversionFactor(double conversionFactor) { - SparkMaxConfig cfg = null; - if (sparkMax instanceof SparkMaxSwerve) { - cfg = ((SparkMaxSwerve) sparkMax).getConfig(); - - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); - } - if (cfg != null) { - cfg.closedLoop.feedbackSensor(FeedbackSensor.kAnalogSensor); - - cfg.signals - .analogVelocityAlwaysOn(true) - .analogVoltageAlwaysOn(true) - .analogPositionAlwaysOn(true) - .analogVoltagePeriodMs(20) - .analogPositionPeriodMs(20) - .analogVelocityPeriodMs(20); - - cfg.analogSensor - .positionConversionFactor(conversionFactor) - .velocityConversionFactor(conversionFactor / 60); - } - if (sparkMax instanceof SparkMaxSwerve) { - ((SparkMaxSwerve) sparkMax).updateConfig(cfg); - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); - } - } - - /** Reset the encoder to factory defaults. */ - @Override - public void factoryDefault() { - // Do nothing - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - // Do nothing - } - - /** - * Configure the absolute encoder to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - if (sparkMax instanceof SparkMaxSwerve) { - SparkMaxConfig cfg = ((SparkMaxSwerve) sparkMax).getConfig(); - cfg.analogSensor.inverted(inverted); - ((SparkMaxSwerve) sparkMax).updateConfig(cfg); - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - SparkMaxConfig cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); - cfg.analogSensor.inverted(inverted); - ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); - } - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - return encoder.getPosition(); - } - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Sets the Absolute Encoder offset at the Encoder Level. - * - * @param offset the offset the Absolute Encoder uses as the zero point. - * @return if setting Absolute Encoder Offset was successful or not. - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - doesNotSupportIntegratedOffsets.set(true); - return false; - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - return encoder.getVelocity(); - } -} diff --git a/src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java b/src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java deleted file mode 100644 index cc2ad1ba..00000000 --- a/src/main/java/swervelib/encoders/SparkMaxEncoderSwerve.java +++ /dev/null @@ -1,190 +0,0 @@ -package swervelib.encoders; - -import com.revrobotics.AbsoluteEncoder; -import com.revrobotics.REVLibError; -import com.revrobotics.spark.SparkAbsoluteEncoder; -import com.revrobotics.spark.SparkMax; -import com.revrobotics.spark.config.SparkMaxConfig; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import java.util.function.Supplier; -import swervelib.motors.SparkMaxBrushedMotorSwerve; -import swervelib.motors.SparkMaxSwerve; -import swervelib.motors.SwerveMotor; - -/** SparkMax absolute encoder, attached through the data port. */ -public class SparkMaxEncoderSwerve extends SwerveAbsoluteEncoder { - - /** The {@link AbsoluteEncoder} representing the duty cycle encoder attached to the SparkMax. */ - public SparkAbsoluteEncoder encoder; - /** An {@link Alert} for if there is a failure configuring the encoder. */ - private Alert failureConfiguring; - /** An {@link Alert} for if there is a failure configuring the encoder offset. */ - private Alert offsetFailure; - /** {@link SparkMaxBrushedMotorSwerve} or {@link SparkMaxSwerve} instance. */ - private SwerveMotor sparkMax; - - /** - * Create the {@link SparkMaxEncoderSwerve} object as a duty cycle from the {@link - * com.revrobotics.spark.SparkMax} motor. - * - * @param motor Motor to create the encoder from. - * @param conversionFactor The conversion factor to set if the output is not from 0 to 360. - */ - public SparkMaxEncoderSwerve(SwerveMotor motor, int conversionFactor) { - failureConfiguring = - new Alert("Encoders", "Failure configuring SparkMax Absolute Encoder", AlertType.kWarning); - offsetFailure = - new Alert("Encoders", "Failure to set Absolute Encoder Offset", AlertType.kWarning); - if (motor.getMotor() instanceof SparkMax) { - sparkMax = motor; - encoder = ((SparkMax) motor.getMotor()).getAbsoluteEncoder(); - setConversionFactor(conversionFactor); - } else { - throw new RuntimeException("Motor given to instantiate SparkMaxEncoder is not a CANSparkMax"); - } - } - - @Override - public void close() { - // SPARK MAX encoder gets closed with the motor - // I don't think an encoder getting closed should - // close the entire motor so i will keep this empty - // sparkFlex.close(); - } - - /** - * Run the configuration until it succeeds or times out. - * - * @param config Lambda supplier returning the error state. - */ - private void configureSparkMax(Supplier config) { - for (int i = 0; i < maximumRetries; i++) { - if (config.get() == REVLibError.kOk) { - return; - } - } - failureConfiguring.set(true); - } - - /** Reset the encoder to factory defaults. */ - @Override - public void factoryDefault() { - // Do nothing - } - - /** Clear sticky faults on the encoder. */ - @Override - public void clearStickyFaults() { - // Do nothing - } - - /** - * Configure the absolute encoder to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - if (sparkMax instanceof SparkMaxSwerve) { - SparkMaxConfig cfg = ((SparkMaxSwerve) sparkMax).getConfig(); - cfg.absoluteEncoder.inverted(inverted); - ((SparkMaxSwerve) sparkMax).updateConfig(cfg); - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - SparkMaxConfig cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); - cfg.absoluteEncoder.inverted(inverted); - ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); - } - } - - /** - * Set the conversion factor of the {@link SparkMaxEncoderSwerve}. - * - * @param conversionFactor Position conversion factor from ticks to unit. - */ - public void setConversionFactor(double conversionFactor) { - // By default the SparkMax relays the info from the duty cycle encoder to the roborio every - // 200ms on CAN frame 5 - // This needs to be set to 20ms or under to properly update the swerve module position for - // odometry - // Configuration taken from 3005, the team who helped develop the Max Swerve: - // https://github.com/FRC3005/Charged-Up-2023-Public/blob/2b6a7c695e23edebafa27a76cf639a00f6e8a3a6/src/main/java/frc/robot/subsystems/drive/REVSwerveModule.java#L227-L244 - // Some of the frames can probably be adjusted to decrease CAN utilization, with 65535 being the - // max. - // From testing, 20ms on frame 5 sometimes returns the same value while constantly powering the - // azimuth but 8ms may be overkill, - // with limited testing 19ms did not return the same value while the module was constatntly - // rotating. - - SparkMaxConfig cfg = null; - if (sparkMax instanceof SparkMaxSwerve) { - cfg = ((SparkMaxSwerve) sparkMax).getConfig(); - - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); - } - if (cfg != null) { - cfg.signals.absoluteEncoderPositionAlwaysOn(true).absoluteEncoderPositionPeriodMs(20); - - cfg.absoluteEncoder - .positionConversionFactor(conversionFactor) - .velocityConversionFactor(conversionFactor / 60); - } - if (sparkMax instanceof SparkMaxSwerve) { - ((SparkMaxSwerve) sparkMax).updateConfig(cfg); - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); - } - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - return encoder.getPosition(); - } - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - @Override - public Object getAbsoluteEncoder() { - return encoder; - } - - /** - * Sets the Absolute Encoder Offset inside of the SparkMax's Memory. - * - * @param offset the offset the Absolute Encoder uses as the zero point. - * @return if setting Absolute Encoder Offset was successful or not. - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - if (sparkMax instanceof SparkMaxSwerve) { - SparkMaxConfig cfg = ((SparkMaxSwerve) sparkMax).getConfig(); - cfg.absoluteEncoder.zeroOffset(offset); - ((SparkMaxSwerve) sparkMax).updateConfig(cfg); - return true; - } else if (sparkMax instanceof SparkMaxBrushedMotorSwerve) { - SparkMaxConfig cfg = ((SparkMaxBrushedMotorSwerve) sparkMax).getConfig(); - cfg.absoluteEncoder.zeroOffset(offset); - ((SparkMaxBrushedMotorSwerve) sparkMax).updateConfig(cfg); - } - return false; - } - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - @Override - public double getVelocity() { - return encoder.getVelocity(); - } -} diff --git a/src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java b/src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java deleted file mode 100644 index a0ff511e..00000000 --- a/src/main/java/swervelib/encoders/SwerveAbsoluteEncoder.java +++ /dev/null @@ -1,64 +0,0 @@ -package swervelib.encoders; - -/** - * Swerve abstraction class to define a standard interface with absolute encoders for swerve - * modules.. - */ -public abstract class SwerveAbsoluteEncoder implements AutoCloseable { - - // This is a bit weird because some encoders are closable - // while some get closed with the motor controller - // so for some encoders this will be an empty function - @Override - public abstract void close(); - - /** - * The maximum amount of times the swerve encoder will attempt to configure itself if failures - * occur. - */ - public final int maximumRetries = 5; - /** Last angle reading was faulty. */ - public boolean readingError = false; - - /** Reset the encoder to factory defaults. */ - public abstract void factoryDefault(); - - /** Clear sticky faults on the encoder. */ - public abstract void clearStickyFaults(); - - /** - * Configure the absolute encoder to read from [0, 360) per second. - * - * @param inverted Whether the encoder is inverted. - */ - public abstract void configure(boolean inverted); - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - public abstract double getAbsolutePosition(); - - /** - * Get the instantiated absolute encoder Object. - * - * @return Absolute encoder object. - */ - public abstract Object getAbsoluteEncoder(); - - /** - * Sets the Absolute Encoder offset at the Encoder Level. - * - * @param offset the offset the Absolute Encoder uses as the zero point in degrees. - * @return if setting Absolute Encoder Offset was successful or not. - */ - public abstract boolean setAbsoluteEncoderOffset(double offset); - - /** - * Get the velocity in degrees/sec. - * - * @return velocity in degrees/sec. - */ - public abstract double getVelocity(); -} diff --git a/src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java b/src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java deleted file mode 100644 index 521faa79..00000000 --- a/src/main/java/swervelib/encoders/TalonSRXEncoderSwerve.java +++ /dev/null @@ -1,82 +0,0 @@ -package swervelib.encoders; - -import com.ctre.phoenix.motorcontrol.FeedbackDevice; -import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX; -import swervelib.motors.SwerveMotor; -import swervelib.motors.TalonSRXSwerve; - -/** Talon SRX attached absolute encoder. */ -public class TalonSRXEncoderSwerve extends SwerveAbsoluteEncoder { - - /** Multiplying by this converts native Talon SRX units into degrees. */ - private final double degreesPerSensorUnit; - /** Reference to a Talon SRX for polling its attached absolute encoder. */ - private final WPI_TalonSRX talon; - - /** - * Creates a {@link TalonSRXEncoderSwerve}. - * - * @param motor motor to poll the sensor from. - * @param feedbackDevice the feedback device the sensor uses e.g. PWM or Analog. - */ - public TalonSRXEncoderSwerve(SwerveMotor motor, FeedbackDevice feedbackDevice) { - if (motor instanceof TalonSRXSwerve talonSRXSwerve) { - talonSRXSwerve.setSelectedFeedbackDevice(feedbackDevice); - this.talon = (WPI_TalonSRX) talonSRXSwerve.getMotor(); - // https://v5.docs.ctr-electronics.com/en/stable/ch14_MCSensor.html#sensor-resolution - degreesPerSensorUnit = - switch (feedbackDevice) { - case Analog -> 360.0 / 1024.0; - default -> 360.0 / 4096.0; - }; - } else { - throw new RuntimeException( - "Motor given to instantiate TalonSRXEncoder is not a WPI_TalonSRX"); - } - } - - @Override - public void close() { - // TalonSRX encoder gets closed with the motor - // I don't think an encoder getting closed should - // close the entire motor so i will keep this empty - // sparkFlex.close(); - } - - @Override - public void factoryDefault() { - // Handled in TalonSRXSwerve - } - - @Override - public void clearStickyFaults() { - // Handled in TalonSRXSwerve - } - - @Override - public void configure(boolean inverted) { - talon.setSensorPhase(inverted); - } - - @Override - public double getAbsolutePosition() { - return (talon.getSelectedSensorPosition() * degreesPerSensorUnit) % 360; - } - - @Override - public Object getAbsoluteEncoder() { - return talon; - } - - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - talon.setSelectedSensorPosition( - talon.getSelectedSensorPosition() + offset / degreesPerSensorUnit); - return true; - } - - @Override - public double getVelocity() { - return talon.getSelectedSensorVelocity() * 10 * degreesPerSensorUnit; - } -} diff --git a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java b/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java deleted file mode 100644 index 6b300077..00000000 --- a/src/main/java/swervelib/encoders/ThriftyNovaEncoderSwerve.java +++ /dev/null @@ -1,110 +0,0 @@ -package swervelib.encoders; - -import com.thethriftybot.Conversion; -import com.thethriftybot.Conversion.PositionUnit; -import com.thethriftybot.Conversion.VelocityUnit; -import com.thethriftybot.ThriftyNova; -import com.thethriftybot.ThriftyNova.EncoderType; -import com.thethriftybot.ThriftyNova.ExternalEncoder; -import swervelib.motors.SwerveMotor; -import swervelib.motors.ThriftyNovaSwerve; - -/** Thrifty Nova absolute encoder, attached through the data port. */ -public class ThriftyNovaEncoderSwerve extends SwerveAbsoluteEncoder { - - /** The absolute encoder is directly interfaced through the Thrifty Nova motor. */ - protected ThriftyNova motor; - /** Inversion state of the attached encoder. */ - protected boolean inverted = false; - /** Offset of the absolute encoder. */ - protected double offset = 0.0; - /** Velocity conversion object for the motor encoder */ - private Conversion velocityConversion; - /** Position conversion object for the motor encoder */ - private Conversion positionConversion; - - /** - * Create the {@link ThriftyNovaEncoderSwerve} object as an absolute encoder from the {@link - * ThriftyNovaSwerve} motor. - * - * @param motor {@link SwerveMotor} through which to interface with the attached encoder . - */ - public ThriftyNovaEncoderSwerve(SwerveMotor motor, String encoderType) { - this.motor = (ThriftyNova) motor.getMotor(); - velocityConversion = new Conversion(VelocityUnit.DEGREES_PER_SEC, EncoderType.ABS); - positionConversion = new Conversion(PositionUnit.DEGREES, EncoderType.ABS); - ExternalEncoder externalEncoderType = ExternalEncoder.valueOf(encoderType); - this.motor.setExternalEncoder(externalEncoderType); - setAbsoluteEncoderOffset(offset); - if (ExternalEncoder.REDUX_ENCODER == externalEncoderType) { - this.motor.setAbsoluteWrapping(true); - } - } - - @Override - public void close() { - // ThriftyNova encoder gets closed with the motor - // I don't think an encoder getting closed should - // close the entire motor so i will keep this empty - } - - /** Set factory default. */ - @Override - public void factoryDefault() {} - - /** Clear sticky faults. */ - @Override - public void clearStickyFaults() {} - - /** - * Configure the absolute encoder. - * - * @param inverted Whether the encoder is inverted. - */ - @Override - public void configure(boolean inverted) { - this.inverted = inverted; - } - - /** - * Get the absolute position of the encoder. - * - * @return Absolute position in degrees from [0, 360). - */ - @Override - public double getAbsolutePosition() { - double rawMotor = motor.getPositionAbs(); - double convertedPosition = positionConversion.fromMotor(rawMotor); - return convertedPosition * (inverted ? -1.0 : 1.0); - } - - /** Get the instantiated absolute encoder Object. */ - @Override - public Object getAbsoluteEncoder() { - return null; - } - - /** - * Set the absolute encoder offset. - * - * @param offset offset in degrees from [0, 360). - * @return true if successful. - */ - @Override - public boolean setAbsoluteEncoderOffset(double offset) { - this.offset = offset; - motor.setAbsOffset((int) offset); - return true; - } - - /** - * Get the absolute encoder velocity. WARNING: Angular velocity is generally not measurable at - * high speeds. - * - * @return Velocity in degrees per second. - */ - @Override - public double getVelocity() { - return velocityConversion.fromMotor(motor.getVelocity()) * (inverted ? -1.0 : 1.0); - } -} diff --git a/src/main/java/swervelib/encoders/package-info.java b/src/main/java/swervelib/encoders/package-info.java deleted file mode 100644 index 6b93a2d5..00000000 --- a/src/main/java/swervelib/encoders/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Absolute encoders for the swerve drive, all implement {@link - * swervelib.encoders.SwerveAbsoluteEncoder}. - */ -package swervelib.encoders; diff --git a/src/main/java/swervelib/imu/ADIS16448Swerve.java b/src/main/java/swervelib/imu/ADIS16448Swerve.java deleted file mode 100644 index 44b14048..00000000 --- a/src/main/java/swervelib/imu/ADIS16448Swerve.java +++ /dev/null @@ -1,120 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.ADIS16448_IMU; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; - -/** IMU Swerve class for the {@link ADIS16448_IMU} device. */ -public class ADIS16448Swerve extends SwerveIMU { - - /** {@link ADIS16448_IMU} device to read the current headings from. */ - private final ADIS16448_IMU imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** Offset for the ADIS16448. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Construct the ADIS16448 imu and reset default configurations. Publish the gyro to the - * SmartDashboard. - */ - public ADIS16448Swerve() { - imu = new ADIS16448_IMU(); - factoryDefault(); - SmartDashboard.putData(imu); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset IMU to factory default. */ - @Override - public void factoryDefault() { - offset = new Rotation3d(0, 0, 0); - imu.calibrate(); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() { - // Do nothing. - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - public Rotation3d getRawRotation3d() { - Rotation3d reading = - new Rotation3d( - Math.toRadians(-imu.getGyroAngleX()), - Math.toRadians(-imu.getGyroAngleY()), - Math.toRadians(-imu.getGyroAngleZ())); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration. - */ - @Override - public Optional getAccel() { - return Optional.of(new Translation3d(imu.getAccelX(), imu.getAccelY(), imu.getAccelZ())); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/ADIS16470Swerve.java b/src/main/java/swervelib/imu/ADIS16470Swerve.java deleted file mode 100644 index 68328dec..00000000 --- a/src/main/java/swervelib/imu/ADIS16470Swerve.java +++ /dev/null @@ -1,121 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.ADIS16470_IMU; -import edu.wpi.first.wpilibj.ADIS16470_IMU.IMUAxis; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; - -/** IMU Swerve class for the {@link ADIS16470_IMU} device. */ -public class ADIS16470Swerve extends SwerveIMU { - - /** {@link ADIS16470_IMU} device to read the current headings from. */ - private final ADIS16470_IMU imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** Offset for the ADIS16470. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Construct the ADIS16470 imu and reset default configurations. Publish the gyro to the - * SmartDashboard. - */ - public ADIS16470Swerve() { - imu = new ADIS16470_IMU(); - offset = new Rotation3d(); - factoryDefault(); - SmartDashboard.putData(imu); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset IMU to factory default. */ - @Override - public void factoryDefault() { - offset = new Rotation3d(0, 0, 0); - imu.calibrate(); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() { - // Do nothing. - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - public Rotation3d getRawRotation3d() { - Rotation3d reading = - new Rotation3d( - Math.toRadians(imu.getAngle(IMUAxis.kRoll)), - Math.toRadians(imu.getAngle(IMUAxis.kPitch)), - Math.toRadians(imu.getAngle(IMUAxis.kYaw))); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - return Optional.of(new Translation3d(imu.getAccelX(), imu.getAccelY(), imu.getAccelZ())); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/ADXRS450Swerve.java b/src/main/java/swervelib/imu/ADXRS450Swerve.java deleted file mode 100644 index 965a9a87..00000000 --- a/src/main/java/swervelib/imu/ADXRS450Swerve.java +++ /dev/null @@ -1,115 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.ADXRS450_Gyro; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; - -/** IMU Swerve class for the {@link ADXRS450_Gyro} device. */ -public class ADXRS450Swerve extends SwerveIMU { - - /** {@link ADXRS450_Gyro} device to read the current headings from. */ - private final ADXRS450_Gyro imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** Offset for the ADXRS450. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Construct the ADXRS450 imu and reset default configurations. Publish the gyro to the - * SmartDashboard. - */ - public ADXRS450Swerve() { - imu = new ADXRS450_Gyro(); - factoryDefault(); - SmartDashboard.putData(imu); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset IMU to factory default. */ - @Override - public void factoryDefault() { - imu.calibrate(); - offset = new Rotation3d(0, 0, 0); // Math.toRadians(-imu.getAngle())); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() { - // Do nothing. - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - public Rotation3d getRawRotation3d() { - Rotation3d reading = new Rotation3d(0, 0, Math.toRadians(-imu.getAngle())); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - return Optional.empty(); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/AnalogGyroSwerve.java b/src/main/java/swervelib/imu/AnalogGyroSwerve.java deleted file mode 100644 index 33206dc1..00000000 --- a/src/main/java/swervelib/imu/AnalogGyroSwerve.java +++ /dev/null @@ -1,120 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.AnalogGyro; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; - -/** Creates a IMU for {@link edu.wpi.first.wpilibj.AnalogGyro} devices, only uses yaw. */ -public class AnalogGyroSwerve extends SwerveIMU { - - /** Gyroscope object. */ - private final AnalogGyro imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** Offset for the analog gyro. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Analog port in which the gyroscope is connected. Can only be attached to analog ports 0 or 1. - * - * @param channel Analog port 0 or 1. - */ - public AnalogGyroSwerve(int channel) { - if (!(channel == 0 || channel == 1)) { - throw new RuntimeException( - "Analog Gyroscope must be attached to port 0 or 1 on the roboRIO.\n"); - } - imu = new AnalogGyro(channel); - factoryDefault(); - SmartDashboard.putData(imu); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset IMU to factory default. */ - @Override - public void factoryDefault() { - imu.calibrate(); - offset = new Rotation3d(0, 0, 0); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() { - // Do nothing. - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - public Rotation3d getRawRotation3d() { - Rotation3d reading = new Rotation3d(0, 0, Math.toRadians(-imu.getAngle())); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - return Optional.empty(); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/CanandgyroSwerve.java b/src/main/java/swervelib/imu/CanandgyroSwerve.java deleted file mode 100644 index 0928e2ce..00000000 --- a/src/main/java/swervelib/imu/CanandgyroSwerve.java +++ /dev/null @@ -1,117 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.RotationsPerSecond; - -import com.reduxrobotics.sensors.canandgyro.Canandgyro; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import java.util.Optional; - -/** SwerveIMU interface for the Boron {@link Canandgyro} by Redux Robotics */ -public class CanandgyroSwerve extends SwerveIMU { - - /** Wait time for status frames to show up. */ - public static double STATUS_TIMEOUT_SECONDS = 0.04; - /** Boron {@link Canandgyro} by Redux Robotics. */ - private final Canandgyro imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, RotationsPerSecond); - /** Offset for the Boron {@link Canandgyro}. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Generate the SwerveIMU for {@link Canandgyro}. - * - * @param canid CAN ID for the Boron {@link Canandgyro} - */ - public CanandgyroSwerve(int canid) { - imu = new Canandgyro(canid); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset {@link Canandgyro} to factory default. */ - @Override - public void factoryDefault() { - imu.resetFactoryDefaults(STATUS_TIMEOUT_SECONDS); - } - - /** Clear sticky faults on {@link Canandgyro}. */ - @Override - public void clearStickyFaults() { - imu.clearStickyFaults(); - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRawRotation3d() { - Rotation3d reading = imu.getRotation3d(); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - - return Optional.of( - new Translation3d(imu.getAccelerationFrame().getValue()).times(9.81 / 16384.0)); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getAngularVelocityYaw()); - } - - /** - * Get the instantiated {@link Canandgyro} IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/NavXSwerve.java b/src/main/java/swervelib/imu/NavXSwerve.java deleted file mode 100644 index f0c50c8e..00000000 --- a/src/main/java/swervelib/imu/NavXSwerve.java +++ /dev/null @@ -1,137 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import com.studica.frc.AHRS; -import com.studica.frc.AHRS.NavXComType; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.Alert; -import edu.wpi.first.wpilibj.Alert.AlertType; -import java.util.Optional; - -/** Communicates with the NavX({@link AHRS}) as the IMU. */ -public class NavXSwerve extends SwerveIMU { - - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** NavX IMU. */ - private AHRS imu; - /** Offset for the NavX. */ - private Rotation3d offset = new Rotation3d(); - /** An {@link Alert} for if there is an error instantiating the NavX. */ - private Alert navXError; - /** Inversion state of the {@link AHRS}. */ - private boolean inverted = false; - - /** - * Constructor for the NavX({@link AHRS}) swerve. - * - * @param port Serial Port to connect to. - */ - public NavXSwerve(NavXComType port) { - navXError = new Alert("IMU", "Error instantiating NavX.", AlertType.kError); - try { - /* Communicate w/navX-MXP via the MXP SPI Bus. */ - /* Alternatively: I2C.Port.kMXP, SerialPort.Port.kMXP or SerialPort.Port.kUSB */ - /* See http://navx-mxp.kauailabs.com/guidance/selecting-an-interface/ for details. */ - imu = new AHRS(port); - factoryDefault(); - } catch (RuntimeException ex) { - navXError.setText("Error instantiating NavX: " + ex.getMessage()); - navXError.set(true); - } - } - - @Override - public void close() { - imu.close(); - } - - /** - * Reset offset to current gyro reading. Does not call NavX({@link AHRS#reset()}) because it has - * been reported to be too slow. - */ - @Override - public void factoryDefault() { - // gyro.reset(); // Reported to be slow - offset = imu.getRotation3d(); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() {} - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - inverted = invertIMU; - // setOffset(getRawRotation3d()); - } - - private Rotation3d negate(Rotation3d rot) { - return new Rotation3d(-rot.getX(), -rot.getY(), -rot.getZ()); - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRawRotation3d() { - return inverted ? negate(imu.getRotation3d()) : imu.getRotation3d(); - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - return Optional.of( - new Translation3d( - imu.getWorldLinearAccelX(), imu.getWorldLinearAccelY(), imu.getWorldLinearAccelZ()) - .times(9.81)); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated NavX({@link AHRS}) IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/Pigeon2Swerve.java b/src/main/java/swervelib/imu/Pigeon2Swerve.java deleted file mode 100644 index bc5bc2bd..00000000 --- a/src/main/java/swervelib/imu/Pigeon2Swerve.java +++ /dev/null @@ -1,151 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import com.ctre.phoenix6.StatusSignal; -import com.ctre.phoenix6.configs.Pigeon2Configuration; -import com.ctre.phoenix6.configs.Pigeon2Configurator; -import com.ctre.phoenix6.hardware.Pigeon2; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.LinearAcceleration; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; -import java.util.function.Supplier; - -/** SwerveIMU interface for the {@link Pigeon2} */ -public class Pigeon2Swerve extends SwerveIMU { - - /** Wait time for status frames to show up. */ - public static double STATUS_TIMEOUT_SECONDS = 0.04; - /** {@link Pigeon2} IMU device. */ - private final Pigeon2 imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** X Acceleration supplier */ - private final Supplier> xAcc; - /** Y Accelleration supplier. */ - private final Supplier> yAcc; - /** Z Acceleration supplier. */ - private final Supplier> zAcc; - /** Offset for the {@link Pigeon2}. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - /** {@link Pigeon2} configurator. */ - private Pigeon2Configurator cfg; - - /** - * Generate the SwerveIMU for {@link Pigeon2}. - * - * @param canid CAN ID for the {@link Pigeon2} - * @param canbus CAN Bus name the {@link Pigeon2} resides on. - */ - public Pigeon2Swerve(int canid, String canbus) { - imu = new Pigeon2(canid, canbus); - this.cfg = imu.getConfigurator(); - xAcc = imu::getAccelerationX; - yAcc = imu::getAccelerationY; - zAcc = imu::getAccelerationZ; - SmartDashboard.putData(imu); - } - - /** - * Generate the SwerveIMU for {@link Pigeon2}. - * - * @param canid CAN ID for the {@link Pigeon2} - */ - public Pigeon2Swerve(int canid) { - this(canid, ""); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset {@link Pigeon2} to factory default. */ - @Override - public void factoryDefault() { - Pigeon2Configuration config = new Pigeon2Configuration(); - - // Compass utilization causes readings to jump dramatically in some cases. - cfg.apply(config.Pigeon2Features.withEnableCompass(false)); - } - - /** Clear sticky faults on {@link Pigeon2}. */ - @Override - public void clearStickyFaults() { - imu.clearStickyFaults(); - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRawRotation3d() { - Rotation3d reading = imu.getRotation3d(); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - return Optional.of( - new Translation3d( - xAcc.get().getValueAsDouble(), - yAcc.get().getValueAsDouble(), - zAcc.get().getValueAsDouble())); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_replace(imu.getAngularVelocityZWorld().refresh().getValue()); - } - - /** - * Get the instantiated {@link Pigeon2} object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/PigeonSwerve.java b/src/main/java/swervelib/imu/PigeonSwerve.java deleted file mode 100644 index 65a44f9f..00000000 --- a/src/main/java/swervelib/imu/PigeonSwerve.java +++ /dev/null @@ -1,121 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import com.ctre.phoenix.sensors.WPI_PigeonIMU; -import edu.wpi.first.math.geometry.Quaternion; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; - -/** SwerveIMU interface for the {@link WPI_PigeonIMU}. */ -public class PigeonSwerve extends SwerveIMU { - - /** {@link WPI_PigeonIMU} IMU device. */ - private final WPI_PigeonIMU imu; - /** Mutable {@link MutAngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** Offset for the {@link WPI_PigeonIMU}. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Generate the SwerveIMU for {@link WPI_PigeonIMU}. - * - * @param canid CAN ID for the {@link WPI_PigeonIMU}, does not support CANBus. - */ - public PigeonSwerve(int canid) { - imu = new WPI_PigeonIMU(canid); - offset = new Rotation3d(); - SmartDashboard.putData(imu); - } - - @Override - public void close() { - imu.close(); - } - - /** Reset IMU to factory default. */ - @Override - public void factoryDefault() { - imu.configFactoryDefault(); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() { - imu.clearStickyFaults(); - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRawRotation3d() { - double[] wxyz = new double[4]; - imu.get6dQuaternion(wxyz); - Rotation3d reading = new Rotation3d(new Quaternion(wxyz[0], wxyz[1], wxyz[2], wxyz[3])); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - short[] initial = new short[3]; - imu.getBiasedAccelerometer(initial); - return Optional.of(new Translation3d(initial[0], initial[1], initial[2]).times(9.81 / 16384.0)); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated {@link WPI_PigeonIMU} IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java b/src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java deleted file mode 100644 index c14d81f8..00000000 --- a/src/main/java/swervelib/imu/PigeonViaTalonSRXSwerve.java +++ /dev/null @@ -1,130 +0,0 @@ -package swervelib.imu; - -import static edu.wpi.first.units.Units.DegreesPerSecond; - -import com.ctre.phoenix.motorcontrol.can.TalonSRX; -import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX; -import com.ctre.phoenix.sensors.WPI_PigeonIMU; -import edu.wpi.first.math.geometry.Quaternion; -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.AngularVelocity; -import edu.wpi.first.units.measure.MutAngularVelocity; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import java.util.Optional; - -/** SwerveIMU interface for the {@link WPI_PigeonIMU}. */ -public class PigeonViaTalonSRXSwerve extends SwerveIMU { - - /** {@link TalonSRX} TalonSRX the IMU is attached to. */ - private final WPI_TalonSRX talon; - - /** {@link WPI_PigeonIMU} IMU device. */ - private final WPI_PigeonIMU imu; - /** Mutable {@link AngularVelocity} for readings. */ - private final MutAngularVelocity yawVel = new MutAngularVelocity(0, 0, DegreesPerSecond); - /** Offset for the {@link WPI_PigeonIMU}. */ - private Rotation3d offset = new Rotation3d(); - /** Inversion for the gyro */ - private boolean invertedIMU = false; - - /** - * Generate the SwerveIMU for {@link WPI_PigeonIMU} attached to a {@link TalonSRX}. - * - * @param canid CAN ID for the {@link TalonSRX} the {@link WPI_PigeonIMU} is attached to, does not - * support CANBus. - */ - public PigeonViaTalonSRXSwerve(int canid) { - talon = new WPI_TalonSRX(canid); - imu = new WPI_PigeonIMU(talon); - offset = new Rotation3d(); - SmartDashboard.putData(imu); - } - - @Override - public void close() { - imu.close(); - talon.close(); - } - - /** Reset IMU to factory default. */ - @Override - public void factoryDefault() { - imu.configFactoryDefault(); - } - - /** Clear sticky faults on IMU. */ - @Override - public void clearStickyFaults() { - imu.clearStickyFaults(); - } - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public void setOffset(Rotation3d offset) { - this.offset = offset; - } - - /** - * Set the gyro to invert its default direction - * - * @param invertIMU invert gyro direction - */ - public void setInverted(boolean invertIMU) { - invertedIMU = invertIMU; - } - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRawRotation3d() { - double[] wxyz = new double[4]; - imu.get6dQuaternion(wxyz); - Rotation3d reading = new Rotation3d(new Quaternion(wxyz[0], wxyz[1], wxyz[2], wxyz[3])); - return invertedIMU ? reading.unaryMinus() : reading; - } - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - @Override - public Rotation3d getRotation3d() { - return getRawRotation3d().rotateBy(offset.unaryMinus()); - } - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - @Override - public Optional getAccel() { - short[] initial = new short[3]; - imu.getBiasedAccelerometer(initial); - return Optional.of(new Translation3d(initial[0], initial[1], initial[2]).times(9.81 / 16384.0)); - } - - @Override - public MutAngularVelocity getYawAngularVelocity() { - return yawVel.mut_setMagnitude(imu.getRate()); - } - - /** - * Get the instantiated {@link WPI_PigeonIMU} IMU object. - * - * @return IMU object. - */ - @Override - public Object getIMU() { - return imu; - } -} diff --git a/src/main/java/swervelib/imu/SwerveIMU.java b/src/main/java/swervelib/imu/SwerveIMU.java deleted file mode 100644 index f95661f8..00000000 --- a/src/main/java/swervelib/imu/SwerveIMU.java +++ /dev/null @@ -1,69 +0,0 @@ -package swervelib.imu; - -import edu.wpi.first.math.geometry.Rotation3d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.units.measure.MutAngularVelocity; -import java.util.Optional; - -/** Swerve IMU abstraction to define a standard interface with a swerve drive. */ -public abstract class SwerveIMU implements AutoCloseable { - - @Override - public abstract void close(); - - /** Reset IMU to factory default. */ - public abstract void factoryDefault(); - - /** Clear sticky faults on IMU. */ - public abstract void clearStickyFaults(); - - /** - * Set the gyro offset. - * - * @param offset gyro offset as a {@link Rotation3d}. - */ - public abstract void setOffset(Rotation3d offset); - - /** - * Set the gyro to invert its default direction. - * - * @param invertIMU gyro direction - */ - public abstract void setInverted(boolean invertIMU); - - /** - * Fetch the {@link Rotation3d} from the IMU without any zeroing. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - public abstract Rotation3d getRawRotation3d(); - - /** - * Fetch the {@link Rotation3d} from the IMU. Robot relative. - * - * @return {@link Rotation3d} from the IMU. - */ - public abstract Rotation3d getRotation3d(); - - /** - * Fetch the acceleration [x, y, z] from the IMU in meters per second squared. If acceleration - * isn't supported returns empty. - * - * @return {@link Translation3d} of the acceleration as an {@link Optional}. - */ - public abstract Optional getAccel(); - - /** - * Fetch the rotation rate from the IMU as {@link MutAngularVelocity} - * - * @return {@link MutAngularVelocity} of the rotation rate. - */ - public abstract MutAngularVelocity getYawAngularVelocity(); - - /** - * Get the instantiated IMU object. - * - * @return IMU object. - */ - public abstract Object getIMU(); -} diff --git a/src/main/java/swervelib/imu/package-info.java b/src/main/java/swervelib/imu/package-info.java deleted file mode 100644 index f32f8463..00000000 --- a/src/main/java/swervelib/imu/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** IMUs used for controlling the robot heading. All implement {@link swervelib.imu.SwerveIMU}. */ -package swervelib.imu; diff --git a/src/main/java/swervelib/math/Matter.java b/src/main/java/swervelib/math/Matter.java deleted file mode 100644 index 8cdca9db..00000000 --- a/src/main/java/swervelib/math/Matter.java +++ /dev/null @@ -1,32 +0,0 @@ -package swervelib.math; - -import edu.wpi.first.math.geometry.Translation3d; - -/** Object with significant mass that needs to be taken into account. */ -public class Matter { - - /** Position in meters from robot center in 3d space. */ - public Translation3d position; - /** Mass in kg of object. */ - public double mass; - - /** - * Construct an object representing some significant matter on the robot. - * - * @param position Position of the matter in meters. - * @param mass Mass in kg. - */ - public Matter(Translation3d position, double mass) { - this.mass = mass; - this.position = position; - } - - /** - * Get the center mass of the object. - * - * @return center mass = position * mass - */ - public Translation3d massMoment() { - return position.times(mass); - } -} diff --git a/src/main/java/swervelib/math/SwerveMath.java b/src/main/java/swervelib/math/SwerveMath.java deleted file mode 100644 index 3c3de296..00000000 --- a/src/main/java/swervelib/math/SwerveMath.java +++ /dev/null @@ -1,394 +0,0 @@ -package swervelib.math; - -import edu.wpi.first.math.controller.SimpleMotorFeedforward; -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.geometry.Rotation2d; -import edu.wpi.first.math.geometry.Translation2d; -import edu.wpi.first.math.geometry.Translation3d; -import edu.wpi.first.math.geometry.Twist2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import java.util.List; -import swervelib.SwerveController; -import swervelib.SwerveModule; -import swervelib.parser.SwerveDriveConfiguration; -import swervelib.parser.SwerveModuleConfiguration; - -/** Mathematical functions which pertain to swerve drive. */ -public class SwerveMath { - - /** - * Calculate the meters per rotation for the integrated encoder. Calculation: (PI * WHEEL DIAMETER - * IN METERS) / (GEAR RATIO * ENCODER RESOLUTION) - * - * @param wheelDiameter Wheel diameter in meters. - * @param driveGearRatio The gear ratio of the drive motor. - * @param pulsePerRotation The number of encoder pulses per rotation. 1 if using an integrated - * encoder. - * @return Meters per rotation for the drive motor. - */ - public static double calculateMetersPerRotation( - double wheelDiameter, double driveGearRatio, double pulsePerRotation) { - return (Math.PI * wheelDiameter) / (driveGearRatio * pulsePerRotation); - } - - /** - * Calculate the meters per rotation for the integrated encoder. Calculation: (PI * WHEEL DIAMETER - * IN METERS) / (GEAR RATIO) - * - * @param wheelDiameter Wheel diameter in meters. - * @param driveGearRatio The gear ratio of the drive motor. - * @return Meters per rotation for the drive motor. - */ - public static double calculateMetersPerRotation(double wheelDiameter, double driveGearRatio) { - return calculateMetersPerRotation(wheelDiameter, driveGearRatio, 1); - } - - /** - * Normalize an angle to be within 0 to 360. - * - * @param angle Angle in degrees. - * @return Normalized angle in degrees. - */ - public static double normalizeAngle(double angle) { - Rotation2d angleRotation = Rotation2d.fromDegrees(angle); - return new Rotation2d(angleRotation.getCos(), angleRotation.getSin()).getDegrees(); - } - - /** - * Algebraically apply a deadband using a piece wise function. - * - * @param value value to apply deadband to. - * @param scaled Use algebra to determine deadband by starting the value at 0 past deadband. - * @param deadband The deadbnad to apply. - * @return Value with deadband applied. - */ - public static double applyDeadband(double value, boolean scaled, double deadband) { - value = Math.abs(value) > deadband ? value : 0; - return scaled - ? ((1 / (1 - deadband)) * (Math.abs(value) - deadband)) * Math.signum(value) - : value; - } - - /** - * Create the drive feedforward for swerve modules. - * - * @param optimalVoltage Optimal voltage to calculate kV (voltage/max Velocity) - * @param maxSpeed Maximum velocity in meters per second to use for the feed forward, should be as - * close to physical max as possible. - * @param wheelGripCoefficientOfFriction Wheel grip coefficient of friction for kA - * (voltage/(cof*9.81)) - * @return Drive feedforward for drive motor on a swerve module. - */ - public static SimpleMotorFeedforward createDriveFeedforward( - double optimalVoltage, double maxSpeed, double wheelGripCoefficientOfFriction) { - double kv = optimalVoltage / maxSpeed; - /// ^ Volt-seconds per meter (max voltage divided by max speed) - double ka = optimalVoltage / calculateMaxAcceleration(wheelGripCoefficientOfFriction); - /// ^ Volt-seconds^2 per meter (max voltage divided by max accel) - return new SimpleMotorFeedforward(0, kv, 0); - } - - /** - * Calculate the degrees per steering rotation for the integrated encoder. Encoder conversion - * values. Drive converts motor rotations to linear wheel distance and steering converts motor - * rotations to module azimuth. - * - * @param angleGearRatio The gear ratio of the steering motor. - * @param pulsePerRotation The number of pulses in a complete rotation for the encoder, 1 if - * integrated. - * @return Degrees per steering rotation for the angle motor. - */ - public static double calculateDegreesPerSteeringRotation( - double angleGearRatio, double pulsePerRotation) { - return 360 / (angleGearRatio * pulsePerRotation); - } - - /** - * Calculate the degrees per steering rotation for the integrated encoder. Encoder conversion - * values. Drive converts motor rotations to linear wheel distance and steering converts motor - * rotations to module azimuth. - * - * @param angleGearRatio The gear ratio of the steering motor. - * @return Degrees per steering rotation for the angle motor. - */ - public static double calculateDegreesPerSteeringRotation(double angleGearRatio) { - return calculateDegreesPerSteeringRotation(angleGearRatio, 1); - } - - /** - * Calculate the maximum angular velocity. - * - * @param maxSpeed Max speed of the robot in meters per second. - * @param furthestModuleX X of the furthest module in meters. - * @param furthestModuleY Y of the furthest module in meters. - * @return Maximum angular velocity in rad/s. - */ - public static double calculateMaxAngularVelocity( - double maxSpeed, double furthestModuleX, double furthestModuleY) { - return maxSpeed / Math.hypot(furthestModuleX, furthestModuleY); - } - - /** - * Calculate the practical maximum acceleration of the robot using the wheel coefficient of - * friction. - * - * @param cof Coefficient of Friction of the wheel grip tape. - * @return Practical maximum acceleration in m/s/s. - */ - public static double calculateMaxAcceleration(double cof) { - return cof * 9.81; - } - - /** - * Calculate the maximum theoretical acceleration without friction. - * - * @param stallTorqueNm Stall torque of driving motor in nM. - * @param gearRatio Gear ratio for driving motor number of motor rotations until one wheel - * rotation. - * @param moduleCount Number of swerve modules. - * @param wheelDiameter Wheel diameter in meters. - * @param robotMass Mass of the robot in kg. - * @return Theoretical maximum acceleration in m/s/s. - */ - public static double calculateMaxAcceleration( - double stallTorqueNm, - double gearRatio, - double moduleCount, - double wheelDiameter, - double robotMass) { - return (stallTorqueNm * gearRatio * moduleCount) / ((wheelDiameter / 2) * robotMass); - } - - /** - * Calculates the maximum acceleration allowed in a direction without tipping the robot. Reads arm - * position from NetworkTables and is passed the direction in question. - * - * @param angle The direction in which to calculate max acceleration, as a Rotation2d. Note that - * this is robot-relative. - * @param matter Matter that the robot is composed of in kg. (Includes chassis) - * @param robotMass The weight of the robot in kg. (Including manipulators, etc). - * @param config The swerve drive configuration. - * @return Maximum acceleration allowed in the robot direction. - */ - private static double calcMaxAccel( - Rotation2d angle, List matter, double robotMass, SwerveDriveConfiguration config) { - // Calculate the vertical mass moment using the floor as the datum. This will be used later to - // calculate max acceleration - Translation3d centerMass = new Translation3d(); - for (Matter object : matter) { - centerMass = centerMass.plus(object.massMoment()); - } - Translation3d robotCG = centerMass.div(robotMass); - Translation2d horizontalCG = robotCG.toTranslation2d(); - - Translation2d projectedHorizontalCg = - new Translation2d( - (angle.getSin() * angle.getCos() * horizontalCG.getY()) - + (Math.pow(angle.getCos(), 2) * horizontalCG.getX()), - (angle.getSin() * angle.getCos() * horizontalCG.getX()) - + (Math.pow(angle.getSin(), 2) * horizontalCG.getY())); - - // Projects the edge of the wheelbase onto the direction line. Assumes the wheelbase is - // rectangular. - // Because a line is being projected, rather than a point, one of the coordinates of the - // projected point is - // already known. - Translation2d projectedWheelbaseEdge; - double angDeg = angle.getDegrees(); - if (angDeg <= 45 && angDeg >= -45) { - SwerveModuleConfiguration conf = getSwerveModule(config.modules, true, true); - projectedWheelbaseEdge = - new Translation2d( - conf.moduleLocation.getX(), conf.moduleLocation.getX() * angle.getTan()); - } else if (135 >= angDeg && angDeg > 45) { - SwerveModuleConfiguration conf = getSwerveModule(config.modules, true, true); - projectedWheelbaseEdge = - new Translation2d( - conf.moduleLocation.getY() / angle.getTan(), conf.moduleLocation.getY()); - } else if (-135 <= angDeg && angDeg < -45) { - SwerveModuleConfiguration conf = getSwerveModule(config.modules, true, false); - projectedWheelbaseEdge = - new Translation2d( - conf.moduleLocation.getY() / angle.getTan(), conf.moduleLocation.getY()); - } else { - SwerveModuleConfiguration conf = getSwerveModule(config.modules, false, true); - projectedWheelbaseEdge = - new Translation2d( - conf.moduleLocation.getX(), conf.moduleLocation.getX() * angle.getTan()); - } - - double horizontalDistance = projectedHorizontalCg.plus(projectedWheelbaseEdge).getNorm(); - return 9.81 * horizontalDistance / robotCG.getZ(); - } - - /** - * Logical inverse of the Pose exponential from 254. Taken from team 3181. - * - * @param transform Pose to perform the log on. - * @return {@link Twist2d} of the transformed pose. - */ - public static Twist2d PoseLog(final Pose2d transform) { - - final double kEps = 1E-9; - final double dtheta = transform.getRotation().getRadians(); - final double half_dtheta = 0.5 * dtheta; - final double cos_minus_one = transform.getRotation().getCos() - 1.0; - double halftheta_by_tan_of_halfdtheta; - if (Math.abs(cos_minus_one) < kEps) { - halftheta_by_tan_of_halfdtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta; - } else { - halftheta_by_tan_of_halfdtheta = - -(half_dtheta * transform.getRotation().getSin()) / cos_minus_one; - } - final Translation2d translation_part = - transform - .getTranslation() - .rotateBy(new Rotation2d(halftheta_by_tan_of_halfdtheta, -half_dtheta)); - return new Twist2d(translation_part.getX(), translation_part.getY(), dtheta); - } - - /** - * Limits a commanded velocity to prevent exceeding the maximum acceleration given by {@link - * SwerveMath#calcMaxAccel}. Note that this takes and returns field-relative velocities. - * - * @param commandedVelocity The desired velocity - * @param fieldVelocity The velocity of the robot within a field relative state. - * @param robotPose The current pose of the robot. - * @param loopTime The time it takes to update the velocity in seconds. Note: this should - * include the 100ms that it takes for a SparkMax velocity to update. - * @param matter Matter that the robot is composed of with position in meters and mass in kg. - * @param robotMass The weight of the robot in kg. (Including manipulators, etc). - * @param config The swerve drive configuration. - * @return The limited velocity. This is either the commanded velocity, if attainable, or the - * closest attainable velocity. - */ - public static Translation2d limitVelocity( - Translation2d commandedVelocity, - ChassisSpeeds fieldVelocity, - Pose2d robotPose, - double loopTime, - double robotMass, - List matter, - SwerveDriveConfiguration config) { - // Get the robot's current field-relative velocity - Translation2d currentVelocity = SwerveController.getTranslation2d(fieldVelocity); - - // Calculate the commanded change in velocity by subtracting current velocity - // from commanded velocity - Translation2d deltaV = commandedVelocity.minus(currentVelocity); - - // Creates an acceleration vector with the direction of delta V and a magnitude - // of the maximum allowed acceleration in that direction - Translation2d maxAccel = - new Translation2d( - calcMaxAccel( - deltaV - // Rotates the velocity vector to convert from field-relative to robot-relative - .rotateBy(robotPose.getRotation().unaryMinus()) - .getAngle(), - matter, - robotMass, - config), - deltaV.getAngle()); - - // Calculate the maximum achievable velocity by the next loop cycle. - // delta V = Vf - Vi = at - Translation2d maxAchievableDeltaVelocity = maxAccel.times(loopTime); - - if (deltaV.getNorm() > maxAchievableDeltaVelocity.getNorm()) { - return maxAchievableDeltaVelocity.plus(currentVelocity); - } else { - // If the commanded velocity is attainable, use that. - return commandedVelocity; - } - } - - /** - * Get the fruthest module from center based on the module locations. - * - * @param modules Swerve module list. - * @param front True = furthest front, False = furthest back. - * @param left True = furthest left, False = furthest right. - * @return Module location which is the furthest from center and abides by parameters. - */ - public static SwerveModuleConfiguration getSwerveModule( - SwerveModule[] modules, boolean front, boolean left) { - Translation2d target = modules[0].configuration.moduleLocation, current, temp; - SwerveModuleConfiguration configuration = modules[0].configuration; - for (SwerveModule module : modules) { - current = module.configuration.moduleLocation; - temp = - front - ? (target.getY() >= current.getY() ? current : target) - : (target.getY() <= current.getY() ? current : target); - target = - left - ? (target.getX() >= temp.getX() ? temp : target) - : (target.getX() <= temp.getX() ? temp : target); - configuration = current.equals(target) ? module.configuration : configuration; - } - return configuration; - } - - /** - * Put an angle within the 360 deg scope of a reference. For example, given a scope reference of - * 756 degrees, assumes the full scope is (720-1080), and places an angle of 22 degrees into it, - * returning 742 deg. - * - *