diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FarLeave.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FarLeave.java index b522fa762ec1..67c546ec8787 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FarLeave.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FarLeave.java @@ -15,35 +15,78 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * FarLeave is a minimal "leave the starting zone" autonomous OpMode. + * + *

This is the simplest autonomous program: the robot simply drives straight + * forward to the parking zone and stops. It is used when the field-side + * ("far") starting position is used and there is no time to implement a more + * complex autonomous routine, or as a guaranteed scoring fallback when more + * complex autos are unreliable. + * + *

The robot starts at field position (0, 0) facing 0° (right), then + * drives straight to ({@link #PARK_X}, {@link #PARK_Y}). Both coordinates + * are tunable at runtime via FTC Dashboard (the class is annotated with + * {@link Config}). + * + *

Registered as {@code "Far Leave Auto"} in the {@code "Autonomous"} group. + */ @Config @Autonomous(name = "Far Leave Auto", group = "Autonomous") public class FarLeave extends LinearOpMode { + + /** + * Target X-coordinate (inches) for the parking position. + * Positive X moves the robot forward (away from the driver station wall). + * Default of 26 inches is enough to cross the field boundary line. + */ public static double PARK_X = 26; + + /** + * Target Y-coordinate (inches) for the parking position. + * Zero keeps the robot on the same lateral track as the starting position. + * Adjust if the robot needs to dodge field elements while leaving. + */ public static double PARK_Y = 0; + /** + * Entry point called by the FTC runtime. + * Builds the park trajectory, waits for start, then executes it. + * + * @throws InterruptedException (implicitly) if the OpMode is interrupted + */ @Override public void runOpMode() { + // Define the starting pose: origin (0,0) facing 0° (pointing in +X direction). Pose2d startPose = new Pose2d(0, 0, Math.toRadians(0)); + // Construct all hardware subsystems via the Hardware container. + // Hardware also builds the MecanumDrive and BotActions. Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); - BotActions botActions = hardware.actions; - MecanumDrive drive = hardware.mecanumDrive; + BotActions botActions = hardware.actions; // action factory (unused here but available) + MecanumDrive drive = hardware.mecanumDrive; // Road Runner drive for trajectory building + // The destination pose: (PARK_X, PARK_Y) still facing 0°. Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(0)); + // Build the single-segment trajectory: strafe straight from startPose to parkPose. + // strafeTo drives along a straight line while maintaining the current robot heading. Action toPark = drive.actionBuilder(startPose) .strafeTo( - parkPose.position + parkPose.position // target x,y; heading is held constant at 0° ) .build(); - waitForStart(); - if (isStopRequested()) return; + waitForStart(); // block here until the referee presses PLAY + if (isStopRequested()) return; // abort immediately if stop was pressed during init + // Execute the parking trajectory. + // ParallelAction with a single child is equivalent to running that child directly; + // the wrapper is kept here for structural consistency with other autonomous OpModes. Actions.runBlocking( new ParallelAction( new SequentialAction( - toPark + toPark // drive forward to the parking position ) ) ); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueClose.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueClose.java index f57d6dfe13db..31f751b7ba59 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueClose.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueClose.java @@ -15,113 +15,285 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * FasterBlueClose — Blue Alliance, Close-Side Autonomous WITHOUT Motif (No Color Matching) + * + *

Alliance: Blue  |  Starting Position: Close side, starting at + * (0, 0, 0°) — facing positive-X (field-forward).

+ * + *

Mission Overview

+ *

This is the blue alliance mirror of {@link FasterRedClose}. The robot: + *

    + *
  1. Drives to the blue-side shooting position at (-17, 40, -50°) while initializing the + * indexer drum to slot "two" and spinning the flywheel. Fires the first artifact after + * a 1.65 s delay.
  2. + *
  3. Performs three intake sweeps toward the positive-X field wall (opposite to red side), + * returning to shoot after each without any color rotation.
  4. + *
  5. Parks in the designated zone.
  6. + *
+ *

+ * + *

Key Mirror Differences from FasterRedClose

+ * + * + * @see FasterRedClose Red alliance mirror of this autonomous + * @see FasterBlueCloseMotif Blue alliance version with Obelisk color matching + * @see BotActions Subsystem action factory + * @see Hardware Hardware initialization + */ @Config @Autonomous(name = "Faster Blue Auto No Motif", group = "Autonomous") public class FasterBlueClose extends LinearOpMode { + /** + * Maximum translational velocity (in/s) during intake corridor sweeps. + * 17 in/s is more conservative than the red counterpart's 30 in/s, providing better + * artifact pickup reliability on the blue side. + */ public static double maxIntakeDrivingVel = 17; + /** + * X-coordinate (inches) of the blue shooting position. + * Negative because the blue alliance starting position is on the left (negative-X) side. + */ public static double SHOOT_X = -17; + + /** Y-coordinate (inches) of the blue shooting position. */ public static double SHOOT_Y = 40; + + /** + * Heading (degrees) of the robot at the blue shooting position. + * -50° faces the scoring targets from the negative-X side of the field, roughly mirroring + * the red side's -142.5° around the field centerline. + */ public static double SHOOT_HEADING_DEG = -50; + + /** + * Heading offset (degrees) applied after the first shot. 0° — no correction needed + * since there is no drum rotation to accumulate drift from. + */ public static double SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT = 0; // -2 + /** + * X-coordinate (inches) of the intake sweep start (blue side, negative-X territory). + */ public static double INTAKE_START_X = -12; + + /** + * X offset (inches) ADDED to {@link #INTAKE_START_X} for row 2. + * Value is negative (-3.0), so adding it shifts start outward toward positive X + * (the blue field wall direction), widening row-2 coverage. + */ public static double INTAKE2_START_OFFSET_X = -3.0; + + /** + * X offset (inches) ADDED to {@link #INTAKE_START_X} for row 3. + * Value is negative (-5.0), providing the widest row-3 start position. + */ public static double INTAKE3_START_OFFSET_X = -5.0; + + /** + * X-coordinate (inches) at the end of the row-1 intake sweep (toward the blue field wall, + * positive X direction). Positive value because blue sweeps toward positive X. + */ public static double INTAKE_END_X = 16; + + /** + * Additional depth (inches) for rows 2 and 3. Negative value (-6.0) is subtracted from + * {@link #INTAKE_END_X}, but in blue's mirrored coordinate system the subtraction produces + * a MORE positive result (e.g. 16 - (-6) = 22), pushing deeper into the field. + */ public static double intake_END_2And3_XOffset = -6.0; + /** Y-coordinate (inches) of the first intake row. */ public static double INTAKE1_Y = 50; + + /** Y-coordinate (inches) of the second intake row. */ public static double INTAKE2_Y = 76.5; + + /** Y-coordinate (inches) of the third intake row. */ public static double INTAKE3_Y = 96; + /** + * X-coordinate (inches) of the gate obstacle on the blue side (positive X, field right). + */ public static double gate_X = 12; + + /** Y-coordinate (inches) of the gate obstacle. */ public static double gate_Y = 60; + /** Dwell time at gate if goToGate action is enabled. Currently unused (commented out). */ public static double gateWaitTime = 1.5; + /** X-coordinate (inches) of the blue parking position (negative X side). */ public static double PARK_X = -6; + + /** Y-coordinate (inches) of the blue parking position. */ public static double PARK_Y = 68; + /** + * Target flywheel speed in RPM. Same as the red no-motif variant (3240). + */ public static int SHOOT_RPM = 3240; + /** + * Delay (seconds) before triggering outtake. 1.65 s matches the red no-motif variant — + * just enough for flywheel spinup without any drum rotation wait. + */ public static double timeUntilStartOuttake = 1.65; // Time until you start the outtake action, which still includes the wait for actuator - // has quick outtake and quick intake + /** + * Initializes hardware, pre-compiles all trajectories, and runs the blue no-motif routine. + * + *

All trajectories mirror the red no-motif variant geometrically: intake corridors sweep + * toward positive X (blue field wall), the shooting pose is at negative X, and the gate + * dodge arc angles in the opposite direction. The timing parameters are identical to the + * red variant.

+ * + *

Execution Order: + *

    + *
  1. {@code toShoot} — Drive to blue shooting pose (-17, 40, -50°); init drum + flywheel; + * fire artifact 1 after 1.65 s.
  2. + *
  3. {@code intake1} — Sweep row 1 (Y ≈ 50 in.) toward positive X at 17 in/s.
  4. + *
  5. {@code backToShoot1} — Return to shoot; fire artifact 2.
  6. + *
  7. {@code intake2} — Sweep row 2 (Y ≈ 76.5 in.).
  8. + *
  9. {@code backToShoot2} — Navigate around blue-side gate; fire artifact 3 (+0.5 s).
  10. + *
  11. {@code intake3} — Sweep row 3 (Y ≈ 96 in.).
  12. + *
  13. {@code backToShoot3} — Return to shoot; fire artifact 4 (+1.0 s).
  14. + *
  15. {@code toPark} — Drive to blue parking zone.
  16. + *
+ *

+ */ @Override public void runOpMode() { + // Robot starts at origin facing 0° (positive-X direction, blue close side). Pose2d startPose = new Pose2d(0, 0, Math.toRadians(0)); + // Initialize all hardware subsystems. Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); + // BotActions wraps subsystem commands as composable Road Runner Actions. BotActions botActions = hardware.actions; + // MecanumDrive for trajectory following. MecanumDrive drive = hardware.mecanumDrive; + // Blue shooting pose: (-17, 40, -50°). On the negative-X (left) side of the field, + // heading -50° faces the scoring targets from the blue alliance position. Pose2d shootingPose = new Pose2d( SHOOT_X, SHOOT_Y, Math.toRadians(SHOOT_HEADING_DEG) ); + // Row-1 intake corridor: sweeps from X=-12 to X=+16 at Y=50, heading = 0°. + // Blue intake sweeps TOWARD positive X (the right/blue field wall), opposite to red. Pose2d intake1PoseStart = new Pose2d(INTAKE_START_X, INTAKE1_Y, Math.toRadians(0)); Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(0)); + // Gate on blue side at (+12, 60, -90°). Positive X reflects the blue-side gate location. Pose2d gate = new Pose2d(gate_X, gate_Y, Math.toRadians(-90)); + // Row-2 intake corridor: start shifted by -3 (outward in negative X), end deeper (+6). Pose2d intake2PoseStart = new Pose2d(INTAKE_START_X + INTAKE2_START_OFFSET_X, INTAKE2_Y, Math.toRadians(0)); Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(0)); + // dodgeGate: -14 in. in X (vs. +14 for red) and -4 in. Y, heading = 40°. + // The negative X shift steers the robot AWAY from the blue-side gate (at +12 X) + // before the spline back to the shooting pose. Pose2d dodgeGate = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset - 14, INTAKE2_Y - 4, Math.toRadians(40)); + // Row-3 intake corridor: start shifted by -5, same end depth as row 2. Pose2d intake3PoseStart = new Pose2d(INTAKE_START_X + INTAKE3_START_OFFSET_X, INTAKE3_Y, Math.toRadians(0)); Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(0)); + // Parking pose: (-6, 68, 0°). On the negative-X side, facing positive X. Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(0)); + // toShoot: Drive from start to the blue shooting pose. Initializes indexer to slot 2 + // and spins flywheel simultaneously, then fires after 1.65 s. Action toShoot = new ParallelAction( + // Single-segment spline-heading trajectory from (0,0,0°) to (-17, 40, -50°). drive.actionBuilder(startPose) .strafeToSplineHeading(shootingPose.position, shootingPose.heading) .build(), + // Spin flywheel to 3240 RPM during drive. botActions.actionStartOuttake(SHOOT_RPM), + // Position the indexer drum to the pre-loaded slot 2. botActions.initializeAuto(Indexer.IndexerState.two), new SequentialAction( + // Wait 1.65 s for arrival and flywheel stabilization. new SleepAction(timeUntilStartOuttake), + // Fire the first pre-loaded artifact. botActions.actionQuickOuttake() ) ); + // intake1: Feedback-driven sweep of row 1 at 17 in/s toward positive X. Action intake1 = botActions.actionIntakeThreeFeedback(shootingPose, intake1PoseStart, intake1PoseEnd, drive, maxIntakeDrivingVel); + // goToGate: Optional route to the blue-side gate before returning to shoot. + // Currently commented out in the active sequence. Action goToGate = drive.actionBuilder(intake1PoseEnd) .strafeToLinearHeading(gate.position, gate.heading) .waitSeconds(gateWaitTime) .build(); + // backToShoot1: Direct return from row-1 end to blue shooting pose. Fires artifact 2. Action backToShoot1 = new ParallelAction( + // Direct spline-heading return from row-1 end (positive X side) to shooting pose. drive.actionBuilder(intake1PoseEnd) // goToGate .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel re-spinup during return drive. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // 1.65 s — short return from row 1. new SleepAction(timeUntilStartOuttake), + // Fire second artifact immediately on arrival. botActions.actionQuickOuttake() ) ); + // intake2: Feedback-driven sweep of row 2 (Y ≈ 76.5 in.) at 17 in/s. Action intake2 = botActions.actionIntakeThreeFeedback(shootingPose, intake2PoseStart, intake2PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot2: Two-segment return from row-2 end; avoids the blue-side gate via dodgeGate. + // dodgeGate shifts -14 in. in X (away from gate at +12 X) then splines to shoot. Action backToShoot2 = new ParallelAction( drive.actionBuilder(intake2PoseEnd) + // Arc through dodgeGate (40°) to clear the blue-side gate obstacle. .strafeToSplineHeading(dodgeGate.position, dodgeGate.heading) + // Then spline-heading into the blue shooting pose. .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel with the two-segment drive. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // +0.5 s extra for the longer two-segment return. new SleepAction(timeUntilStartOuttake + 0.5), + // Fire third artifact. botActions.actionQuickOuttake() ) ); @@ -145,44 +317,57 @@ public void runOpMode() { ) );*/ + // intake3: Feedback-driven sweep of row 3 (Y ≈ 96 in.) at 17 in/s. Action intake3 = botActions.actionIntakeThreeFeedback(shootingPose, intake3PoseStart, intake3PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot3: Direct return from row-3 end to the shooting pose. + // No gate dodge needed — row 3 is far enough from the gate that a direct path clears it. Action backToShoot3 = new ParallelAction( + // Single-segment spline-heading return from row-3 end. drive.actionBuilder(intake3PoseEnd) .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // +1.0 s extra for the longest return trip (from row 3). new SleepAction(timeUntilStartOuttake + 1.0), + // Fire fourth and final artifact. botActions.actionQuickOuttake() ) ); + // toPark: Final spline from the shooting pose to the blue parking zone. Action toPark = drive.actionBuilder(shootingPose) .strafeToSplineHeading( - parkPose.position, - parkPose.heading + parkPose.position, // Target: (-6, 68) + parkPose.heading // Final heading: 0° ) .build(); + // Wait for the Drive Station "Play" button. waitForStart(); + // Exit immediately if emergency-stopped before match start. if (isStopRequested()) return; + // Execute the full routine, blocking until all actions complete. Actions.runBlocking( new ParallelAction( + // Thread 1 — Periodic: telemetry and sensor maintenance. botActions.actionPeriodic(), + // Thread 2 — Main sequential mission. new SequentialAction( - toShoot, - intake1, - /*goToGate,*/ - backToShoot1, - intake2, - backToShoot2, - intake3, - backToShoot3, - toPark + toShoot, // Drive to blue shooting pose; init; fire artifact 1 + intake1, // Sweep row 1 (Y ≈ 50 in.) toward positive X + /*goToGate,*/ // (Disabled) Optional gate approach + backToShoot1, // Return to shoot; fire artifact 2 + intake2, // Sweep row 2 (Y ≈ 76.5 in.) + backToShoot2, // Dodge blue-side gate; fire artifact 3 + intake3, // Sweep row 3 (Y ≈ 96 in.) + backToShoot3, // Return to shoot; fire artifact 4 + toPark // Drive to blue parking zone ) ) ); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueCloseMotif.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueCloseMotif.java index 0a7989acb593..d2201212711a 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueCloseMotif.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterBlueCloseMotif.java @@ -16,82 +16,237 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + *

FasterBlueCloseMotif – Blue Alliance Close-Side Autonomous with Motif Detection

+ * + *

This is the primary high-cycle autonomous OpMode for Team 5273 "Decode" on the + * Blue Alliance, close (front) side of the field. It implements a full + * three-cycle intake-and-shoot routine combined with Limelight-based Motif (AprilTag / + * colored-pattern) detection so the robot scores artifacts in the correct scoring positions.

+ * + *

High-Level Autonomous Sequence

+ *
    + *
  1. Initialization – Hardware is set up; the indexer is pre-loaded to slot two; + * the flywheel spins up to {@link #SHOOT_RPM} RPM in parallel with driving.
  2. + *
  3. Drive to Obelisk & Shoot (#1) – The robot strafes through the obelisk + * scan position so the Limelight camera can detect the Motif ID, then drives to the + * shooting pose. While travelling, the indexer rotates to the correct color slot, + * and a quick outtake fires the first artifact.
  4. + *
  5. Intake Cycle 1 – The robot drives to the first intake row (INTAKE1_Y), + * uses sensor feedback to confirm three artifacts are loaded, then returns to the + * shooting pose and fires again (second shot).
  6. + *
  7. Intake Cycle 2 – Repeats the intake process at a deeper row (INTAKE2_Y). + * The return path includes a dodge waypoint to avoid the gate obstacle before + * returning to the shooting pose for the third shot.
  8. + *
  9. Intake Cycle 3 – Deepest intake row (INTAKE3_Y), followed by a fourth + * shot and final intake-passive state.
  10. + *
  11. Park – Robot drives to the designated parking zone.
  12. + *
+ * + *

Parallelism Strategy

+ *

Road Runner's {@link ParallelAction} is used extensively so that the flywheel + * spin-up, indexer rotation, and drivetrain movement all happen simultaneously, saving + * several seconds of cycle time. The {@link #timeUntilStartOuttake} constant is the + * critical tuning value that determines when the outtake sequence begins relative to the + * start of the drive segment – too early and the robot fires before reaching the goal; + * too late and time is wasted waiting.

+ * + *

Tunable Parameters (FTC Dashboard / @Config)

+ *

All {@code public static} fields are exposed to FTC Dashboard via the {@link Config} + * annotation, enabling real-time tuning without recompiling:

+ * + * + *

Key Dependencies

+ * + * + * @see FarLeave + * @see TestingOpmode + */ @Config @Autonomous(name = "Faster Blue Auto With Motif", group = "Autonomous") public class FasterBlueCloseMotif extends LinearOpMode { + /** Maximum translational velocity (inches/sec) allowed while the intake is running. + * Slowing down during intake sweeps improves the chance of picking up artifacts. */ public static double maxIntakeDrivingVel = 15; + /** X coordinate (inches) of the intermediate waypoint used to pass near the Obelisk + * so the Limelight camera can scan the Motif pattern. Negative = toward the alliance wall. */ public static double OBELISK_X = -19.5; + + /** Y coordinate (inches) of the Obelisk scan waypoint. */ public static double OBELISK_Y = 13; + + /** Heading (degrees) the robot faces while passing through the Obelisk scan waypoint. + * A small negative angle keeps the camera pointed toward the Motif. */ public static double OBELISK_HEADING_DEG = -15; + /** X coordinate (inches) of the shooting pose – where the robot stops to fire artifacts. */ public static double SHOOT_X = -17; + + /** Y coordinate (inches) of the shooting pose. */ public static double SHOOT_Y = 40; + + /** Heading (degrees) of the shooting pose, angled toward the high goal. */ public static double SHOOT_HEADING_DEG = -45; + + /** Small heading offset (degrees) added to the shooting pose on every cycle after the + * first shot. This compensates for accumulated odometry drift so subsequent shots + * remain on target. Positive = rotate slightly counter-clockwise. */ public static double SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT = 2; // 2 + /** Starting X coordinate (inches) for the first intake sweep. The robot begins the + * intake motion here (facing 0°) and drives forward to {@link #INTAKE_END_X}. */ public static double INTAKE_START_X = -12; + + /** Additional X offset (inches) applied to the intake 2 start position so the robot + * begins the second sweep slightly further forward, accounting for field geometry. */ public static double INTAKE2_START_OFFSET_X = 3.0; + + /** Additional X offset (inches) for the intake 3 start position (deepest sweep). */ public static double INTAKE3_START_OFFSET_X = 5.0; + + /** X coordinate (inches) at which the robot ends each intake sweep for row 1. + * The intake should have collected artifacts by the time the robot reaches here. */ public static double INTAKE_END_X = 18.5; + + /** Extra X distance (inches) added to INTAKE_END_X for intake rows 2 and 3, because + * those rows are deeper in the field and require the robot to travel further. */ public static double intake_END_2And3_XOffset = 7.5; + /** Y coordinate (inches) of the first intake row – closest to the wall. */ public static double INTAKE1_Y = 52; + + /** Y coordinate (inches) of the second intake row. */ public static double INTAKE2_Y = 76; + + /** Y coordinate (inches) of the third (deepest) intake row. */ public static double INTAKE3_Y = 96; + /** X coordinate (inches) of the gate/ramp position used in the unused {@code goToGate} path. */ public static double gate_X = 12; + + /** Y coordinate (inches) of the gate/ramp position. */ public static double gate_Y = 60; + /** How many seconds the robot waits at the gate position (used in the currently bypassed + * {@code goToGate} action). */ public static double gateWaitTime = 1.5; + /** X coordinate (inches) of the final parking pose at the end of autonomous. */ public static double PARK_X = -6; + + /** Y coordinate (inches) of the final parking pose. */ public static double PARK_Y = 68; + /** Target flywheel speed in RPM. 3580 RPM is tuned for consistent shot trajectory + * at the shooting pose distance from the goal. */ public static int SHOOT_RPM = 3580; + /** Seconds after the start of each drive-to-shoot segment to wait before + * triggering the outtake sequence. This value must be slightly less than the + * expected drive time so the indexer finishes rotating before the robot arrives. + * Includes the internal wait for the actuator/gate to open. */ public static double timeUntilStartOuttake = 2.75; // Time until you start the outtake action, which still includes the wait for actuator // has quick outtake and quick intake // bunch of compensations for bad rr + /** + * Main autonomous execution method called by the FTC SDK when the OpMode starts. + * + *

All trajectory construction and action composition happens here before + * {@code waitForStart()} so that the robot is ready to execute instantly when the + * match begins. After the start signal the pre-built action tree is handed to + * {@link Actions#runBlocking} which drives the robot through the entire sequence.

+ * + *

Threading note: Road Runner actions run on the main loop thread. + * {@link ParallelAction} interleaves actions by calling each one's {@code run()} + * method every loop iteration – they are cooperative, not truly parallel threads.

+ */ @Override public void runOpMode() { + // The robot starts facing 180° (toward the opposing alliance wall on the blue side). + // All coordinates below are relative to this starting pose. Pose2d startPose = new Pose2d(0, 0, Math.toRadians(180)); + // Initialise all hardware (motors, servos, sensors) using the FTC hardwareMap. + // The Hardware class also wires up the Road Runner localizer with the start pose. Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); + + // BotActions provides high-level composite actions (intake, outtake, indexer, etc.) BotActions botActions = hardware.actions; + + // MecanumDrive provides trajectory building and following for the mecanum drivetrain. MecanumDrive drive = hardware.mecanumDrive; + // Intermediate scan waypoint: the robot passes close to the Obelisk structure so + // the Limelight camera captures the Motif (AprilTag-like pattern) and determines + // which slot colour should be shot first. Pose2d obeliskPose = new Pose2d( OBELISK_X, OBELISK_Y, Math.toRadians(OBELISK_HEADING_DEG) ); + // The pose where the robot stops (or near-stops) to fire artifacts into the goal. + // The heading is angled at -45° to aim the flywheel toward the high scoring target. Pose2d shootingPose = new Pose2d( SHOOT_X, SHOOT_Y, Math.toRadians(SHOOT_HEADING_DEG) ); + // Intake row 1: start and end poses. The robot faces 0° (toward the field centre) + // during the intake sweep so the intake funnel is in the correct orientation. Pose2d intake1PoseStart = new Pose2d(INTAKE_START_X, INTAKE1_Y, Math.toRadians(0)); Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(0)); + + // Gate pose (90° heading = robot facing sideways toward the gate mechanism). + // Used in the currently bypassed goToGate action. Pose2d gate = new Pose2d(gate_X, gate_Y, Math.toRadians(90)); + // Intake row 2 poses. The start X is shifted forward by INTAKE2_START_OFFSET_X + // because row 2 artifacts are positioned deeper in the field. Pose2d intake2PoseStart = new Pose2d(INTAKE_START_X + INTAKE2_START_OFFSET_X, INTAKE2_Y, Math.toRadians(0)); Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X + intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(0)); + + // Intermediate waypoint used in backToShoot2 to steer around the physical gate + // obstacle that sits between the intake end and the shooting pose. The robot + // first drives to this dodge point before curving toward the shooting pose. Pose2d dodgeGate = new Pose2d(INTAKE_END_X + intake_END_2And3_XOffset - 16, INTAKE2_Y + 4, Math.toRadians(20)); + // Intake row 3 poses – the deepest intake position on the field. Pose2d intake3PoseStart = new Pose2d(INTAKE_START_X + INTAKE3_START_OFFSET_X, INTAKE3_Y, Math.toRadians(0)); Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X + intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(0)); + // Final park position after all shooting cycles are complete. Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(0)); // Remove actionStartOuttake in toShoot when adding this + // Startup parallel action: initialise the indexer to slot two (pre-loaded artifact) + // while simultaneously spinning the flywheel up to shoot RPM, saving ~1 second. Action start = new ParallelAction( - botActions.initializeAuto(Indexer.IndexerState.two), - botActions.actionStartOuttake(SHOOT_RPM) + botActions.initializeAuto(Indexer.IndexerState.two), // rotate indexer drum to slot 2 + botActions.actionStartOuttake(SHOOT_RPM) // spin flywheel to target RPM ); /* @@ -108,70 +263,92 @@ public void runOpMode() { */ // BE CAREFUL ABOUT THE OBELISK SCANNING AND COLOR ROTATING TIMING + // Drive from start → obelisk scan waypoint → shooting pose, while simultaneously: + // • spinning up the flywheel + // • waiting for the obelisk ID to be detected, then rotating the indexer to the + // correct colour slot, and finally firing the first quick outtake + // The 0.5-second lead time on the sleep ensures the indexer finishes rotating + // before the robot fully arrives at the shooting pose. Action toShoot = new ParallelAction( drive.actionBuilder(startPose) // obeliskPose - .strafeTo(obeliskPose.position) - .strafeToSplineHeading(shootingPose.position, shootingPose.heading) + .strafeTo(obeliskPose.position) // pass near obelisk for camera scan + .strafeToSplineHeading(shootingPose.position, shootingPose.heading) // curve into shooting pose .build(), - botActions.actionStartOuttake(SHOOT_RPM), + botActions.actionStartOuttake(SHOOT_RPM), // spin flywheel in parallel with driving new SequentialAction( - new SleepAction(timeUntilStartOuttake - 0.5), - botActions.rotateToMotifColorBeforeOuttake(0, botActions::getObeliskId, 2), - new SleepAction(0.5), - botActions.actionQuickOuttake() + new SleepAction(timeUntilStartOuttake - 0.5), // wait until robot is nearly at goal + botActions.rotateToMotifColorBeforeOuttake(0, botActions::getObeliskId, 2), // rotate indexer slot 0 to Motif-detected colour + new SleepAction(0.5), // brief pause to let indexer settle + botActions.actionQuickOuttake() // fire first artifact ) ); + // First intake sweep: uses sensor feedback (actionIntakeThreeFeedback) to drive + // from the shooting pose into row 1 and back-detect when three artifacts are loaded. Action intake1 = botActions.actionIntakeThreeFeedback(shootingPose, intake1PoseStart, intake1PoseEnd, drive, maxIntakeDrivingVel); + // Build a trajectory to the gate position and wait there (bypassed in the active run). + // Kept for reference / future use if the gate strategy is re-enabled. Action goToGate = drive.actionBuilder(intake1PoseEnd) - .strafeToLinearHeading(gate.position, gate.heading) - .waitSeconds(gateWaitTime) + .strafeToLinearHeading(gate.position, gate.heading) // drive to gate with final linear heading + .waitSeconds(gateWaitTime) // wait at gate (e.g. for gate to open) .build(); + // Return from intake row 1 to shooting pose. The heading is slightly offset from + // the base shooting heading to compensate for drift accumulated during intake cycle 1. + // Parallel: spin up flywheel, then after delay rotate indexer slot 1 to correct + // colour, wait for robot to arrive, fire second artifact, set intake to passive. Action backToShoot1 = new ParallelAction( drive.actionBuilder(intake1PoseEnd) // goToGate .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), //botActions.actionSetIntakeReverse(), - botActions.actionStartOuttake(SHOOT_RPM), + botActions.actionStartOuttake(SHOOT_RPM), // re-spin flywheel (may have slowed during intake) new SequentialAction( - new SleepAction(timeUntilStartOuttake - 1), - botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 2), - new SleepAction(1), - botActions.actionQuickOuttake(), - botActions.actionSetIntakePassive() + new SleepAction(timeUntilStartOuttake - 1), // wait for robot to near shooting pose + botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 2),// rotate indexer slot 1 to correct colour + new SleepAction(1), // wait for indexer to settle + botActions.actionQuickOuttake(), // fire second artifact + botActions.actionSetIntakePassive() // set intake to passive (no active spin) ready for next cycle ) ); + // Second intake sweep: row 2 (deeper in field). Uses the same sensor-feedback + // approach; intake start/end X offsets account for the deeper artifact positions. Action intake2 = botActions.actionIntakeThreeFeedback(shootingPose, intake2PoseStart, intake2PoseEnd, drive, maxIntakeDrivingVel); + // Return from intake row 2 to shooting pose, routing through dodgeGate waypoint + // to avoid the physical gate obstacle. Uses strafeTo for the dodge, then + // strafeToSplineHeading for a smooth curve into the shooting heading. Action backToShoot2 = new ParallelAction( drive.actionBuilder(intake2PoseEnd) - .strafeTo(dodgeGate.position) + .strafeTo(dodgeGate.position) // drive around gate obstacle .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), //botActions.actionSetIntakeReverse(), - botActions.actionStartOuttake(SHOOT_RPM), + botActions.actionStartOuttake(SHOOT_RPM), // spin up flywheel for third shot new SequentialAction( - new SleepAction(timeUntilStartOuttake - 1), - botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 2), - new SleepAction(1), - botActions.actionQuickOuttake(), - botActions.actionSetIntakePassive() + new SleepAction(timeUntilStartOuttake - 1), // wait until nearly at shooting pose + botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 2),// rotate indexer slot 1 to correct colour + new SleepAction(1), // settle pause + botActions.actionQuickOuttake(), // fire third artifact + botActions.actionSetIntakePassive() // reset intake to passive ) ); + // Alternative spline-based return from intake row 2 (not used in the active run – + // kept as a reference/fallback). Uses splineTo for a smoother arc around the dodge + // waypoint and splineToSplineHeading for a tangent-continuous arrival at shooting pose. Action backToShoot2Spline = new ParallelAction( drive.actionBuilder(intake2PoseEnd) //.setTangent() - .splineTo(dodgeGate.position, dodgeGate.heading) + .splineTo(dodgeGate.position, dodgeGate.heading) // smooth spline arc around gate .splineToSplineHeading( new Pose2d( shootingPose.position, @@ -182,36 +359,41 @@ public void runOpMode() { .build(), //botActions.actionSetIntakeReverse(), - botActions.actionStartOuttake(SHOOT_RPM), + botActions.actionStartOuttake(SHOOT_RPM), // spin up flywheel new SequentialAction( new SleepAction(timeUntilStartOuttake - 1), - botActions.rotateToMotifColorBeforeOuttake(2, botActions::getObeliskId, 2), + botActions.rotateToMotifColorBeforeOuttake(2, botActions::getObeliskId, 2), // slot 2 for third cycle new SleepAction(1), botActions.actionQuickOuttake(), botActions.actionSetIntakePassive() ) ); + // Third intake sweep at the deepest row. Deepest X start offset applied. Action intake3 = botActions.actionIntakeThreeFeedback(shootingPose, intake3PoseStart, intake3PoseEnd, drive, maxIntakeDrivingVel); + // Return from intake row 3 to shooting pose for the fourth and final shot. + // No dodge waypoint needed from row 3 (different field geometry). Action backToShoot3 = new ParallelAction( drive.actionBuilder(intake3PoseEnd) .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), //botActions.actionSetIntakeReverse(), - botActions.actionStartOuttake(SHOOT_RPM), + botActions.actionStartOuttake(SHOOT_RPM), // spin up flywheel for final shot new SequentialAction( new SleepAction(timeUntilStartOuttake - 1), - botActions.rotateToMotifColorBeforeOuttake(3, botActions::getObeliskId, 2), + botActions.rotateToMotifColorBeforeOuttake(3, botActions::getObeliskId, 2), // slot 3 for final cycle new SleepAction(1), - botActions.actionQuickOuttake(), - botActions.actionSetIntakePassive() + botActions.actionQuickOuttake(), // fire final artifact + botActions.actionSetIntakePassive() // disable intake after last cycle ) ); + // Drive from the shooting pose to the parking zone using a spline heading transition + // so the robot arrives facing the correct direction for the end-game. Action toPark = drive.actionBuilder(shootingPose) .strafeToSplineHeading( parkPose.position, @@ -219,28 +401,41 @@ public void runOpMode() { ) .build(); + // Block until the driver presses START on the Driver Station. waitForStart(); + + // Immediately exit if the stop button was pressed instead of start (e.g. e-stop). if (isStopRequested()) return; + // Execute the entire autonomous routine. Actions.runBlocking loops until every + // action in the tree returns false (i.e. finished). Actions.runBlocking( new ParallelAction( + // Lane 1: After a short 0.5-second delay (to let the robot move away from + // the start wall and get a clear camera view), trigger the Limelight scan + // of the Obelisk to identify the Motif colour pattern. new SequentialAction( - new SleepAction(0.5), - botActions.actionScanObelisk() + new SleepAction(0.5), // wait for robot to clear the starting wall + botActions.actionScanObelisk() // capture Motif ID from Limelight ), + + // Lane 2: Run the periodic hardware update loop (telemetry, sensor polling) + // throughout the entire autonomous period. botActions.actionPeriodic(), + + // Lane 3: Main robot drive sequence executed in order. new SequentialAction( - new InstantAction(() -> drive.localizer.setPose(startPose)), - start, - //toObelisk, - toShoot, - intake1, - backToShoot1, - intake2, - backToShoot2, - intake3, - backToShoot3, - toPark + new InstantAction(() -> drive.localizer.setPose(startPose)), // reset odometry to known start pose + start, // initialise indexer + spin up flywheel + //toObelisk, // (disabled) original separate obelisk-scan drive action + toShoot, // drive through obelisk → shoot first artifact + intake1, // sweep row 1 intake + backToShoot1, // return → shoot second artifact + intake2, // sweep row 2 intake + backToShoot2, // return (via gate dodge) → shoot third artifact + intake3, // sweep row 3 intake (deepest) + backToShoot3, // return → shoot fourth artifact + toPark // drive to parking zone ) ) ); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedClose.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedClose.java index 3f803f965ac0..9792d3a21bd0 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedClose.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedClose.java @@ -15,113 +15,288 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * FasterRedClose — Red Alliance, Close-Side Autonomous WITHOUT Motif (No Obelisk Color Matching) + * + *

Alliance: Red  |  Starting Position: Close side, facing + * field-forward at 0° (positive-X direction).

+ * + *

Mission Overview

+ *

This is the simpler, faster variant of the red close-side autonomous. Unlike the "Motif" + * variants, this OpMode does NOT scan the Obelisk AprilTag or rotate the indexer drum to + * match required colors. Instead it fires whatever artifact is currently loaded in slot "two" + * unconditionally after each intake cycle. This removes ~1–2 seconds of drum rotation and + * confirmation time per shot, allowing faster cycle times at the cost of color-matching: + *

    + *
  1. Initializes the indexer to slot "two" and spins the flywheel to {@link #SHOOT_RPM} RPM + * during the initial drive to the shooting position.
  2. + *
  3. Fires the first pre-loaded artifact after a 1.65 s delay.
  4. + *
  5. Performs three intake sweeps (rows at Y ≈ 50, 76.5, and 96 in.) and returns to shoot + * after each, firing immediately on arrival without any color rotation.
  6. + *
  7. Parks in the designated zone.
  8. + *
+ *

+ * + *

Key Differences from Motif Variants

+ * + * + * @see FasterRedCloseMotif Motif (color-matching) version for the red close side + * @see FasterBlueClose Blue alliance mirror of this autonomous + * @see BotActions Subsystem action factory + * @see Hardware Hardware initialization + */ @Config @Autonomous(name = "Faster Red Auto No Motif", group = "Autonomous") public class FasterRedClose extends LinearOpMode { + /** + * Maximum translational velocity (in/s) during intake corridor sweeps. + * 30 in/s prioritises speed — no color matching is needed, so the robot can afford + * faster transit without worrying about losing time to drum rotation. + */ public static double maxIntakeDrivingVel = 30; + /** X-coordinate (inches) of the shooting position. */ public static double SHOOT_X = 17; + + /** Y-coordinate (inches) of the shooting position. */ public static double SHOOT_Y = 40; + + /** + * Heading (degrees) of the robot at the shooting position. + * At -142.5° the robot aims toward the scoring targets from the fixed drum position + * (no rotation means the artifact exits at a predictable angle every time). + */ public static double SHOOT_HEADING_DEG = -142.5; + + /** + * Heading offset (degrees) applied after the first shot. Set to 0° — since there is no + * drum rotation drift, the same heading is equally accurate for all subsequent shots. + */ public static double SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT = 0; + /** X-coordinate (inches) of the intake sweep start for all rows. */ public static double INTAKE_START_X = 12; + + /** + * X offset (inches) ADDED to {@link #INTAKE_START_X} for row 2. + * Note: this is added (not subtracted) — +3.0 shifts the start outward toward negative X, + * widening coverage for row-2 artifacts in this field configuration. + */ public static double INTAKE2_START_OFFSET_X = 3.0; + + /** + * X offset (inches) ADDED to {@link #INTAKE_START_X} for row 3. + * Same additive convention as row 2; +5.0 provides the widest row-3 start position. + */ public static double INTAKE3_START_OFFSET_X = 5.0; + + /** + * X-coordinate (inches) at the end of the row-1 intake sweep (toward the field wall). + */ public static double INTAKE_END_X = -16; + + /** + * Additional depth (inches) subtracted from {@link #INTAKE_END_X} for rows 2 and 3, + * giving a deeper sweep to cover all artifacts in later rows. + */ public static double intake_END_2And3_XOffset = 6.0; + /** Y-coordinate (inches) of the first intake row. */ public static double INTAKE1_Y = 50; + + /** Y-coordinate (inches) of the second intake row. */ public static double INTAKE2_Y = 76.5; + + /** Y-coordinate (inches) of the third intake row. */ public static double INTAKE3_Y = 96; + /** X-coordinate (inches) of the gate obstacle reference. */ public static double gate_X = -12; + + /** Y-coordinate (inches) of the gate obstacle reference. */ public static double gate_Y = 60; + /** + * Dwell time (seconds) at the gate position for the optional {@code goToGate} action. + * {@code goToGate} is defined but commented out in the active sequence. + */ public static double gateWaitTime = 1.5; + /** X-coordinate (inches) of the final parking position. */ public static double PARK_X = 6; + + /** Y-coordinate (inches) of the final parking position. */ public static double PARK_Y = 68; + /** + * Target flywheel speed in RPM. + * 3240 RPM — lower than the motif variants (3580) and calibrated for the fixed drum + * position used in this no-rotation variant. + */ public static int SHOOT_RPM = 3240; + /** + * Delay (seconds) before triggering the outtake sequence. + * At 1.65 s this is short — just enough for flywheel spinup and robot settle. + * No extra time is reserved for drum rotation (there is none in this variant). + * Subsequent shots: backToShoot2 adds +0.5 s; backToShoot3 adds +1.0 s to accommodate + * the longer return paths from rows 2 and 3. + */ public static double timeUntilStartOuttake = 1.65; // Time until you start the outtake action, which still includes the wait for actuator - // has quick outtake and quick intake + /** + * Initializes hardware, pre-compiles trajectories, and runs the full no-motif routine. + * + *

Execution Order: + *

    + *
  1. {@code toShoot} — Drive to shooting pose; set indexer to slot 2; fire artifact 1.
  2. + *
  3. {@code intake1} — Sweep row 1 (Y ≈ 50 in.) at 30 in/s.
  4. + *
  5. {@code backToShoot1} — Return to shooting pose; fire artifact 2 immediately.
  6. + *
  7. {@code intake2} — Sweep row 2 (Y ≈ 76.5 in.).
  8. + *
  9. {@code backToShoot2} — Navigate around gate via dodgeGate; fire artifact 3 (+0.5 s).
  10. + *
  11. {@code intake3} — Sweep row 3 (Y ≈ 96 in.).
  12. + *
  13. {@code backToShoot3} — Return to shooting pose; fire artifact 4 (+1.0 s).
  14. + *
  15. {@code toPark} — Drive to parking zone.
  16. + *
+ * {@code actionPeriodic} runs as a persistent parallel thread for telemetry/sensor maintenance. + * No localizer re-seed is performed here (unlike the motif variants) since the no-motif + * start pose is also (0, 0, 0°) and no pre-match movement is expected.

+ */ @Override public void runOpMode() { + // Robot starts at origin facing 0° (positive-X / field-forward direction). Pose2d startPose = new Pose2d(0, 0, Math.toRadians(0)); + // Initialize all hardware subsystems: drive, indexer, intake, flywheel, camera. Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); + // BotActions provides Road Runner Action wrappers for every subsystem command. BotActions botActions = hardware.actions; + // MecanumDrive handles mecanum-wheel trajectory following. MecanumDrive drive = hardware.mecanumDrive; + // Shooting pose: (17, 40, -142.5°). The heading is tuned for the fixed drum slot's + // artifact exit angle — no rotation means every shot has the same geometry. Pose2d shootingPose = new Pose2d( SHOOT_X, SHOOT_Y, Math.toRadians(SHOOT_HEADING_DEG) ); + // Row-1 intake corridor: sweeps from X=12 to X=-16 at Y=50, heading = 180°. Pose2d intake1PoseStart = new Pose2d(INTAKE_START_X, INTAKE1_Y, Math.toRadians(180)); Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(180)); + // Gate obstacle at (-12, 60, -90°). The -90° heading reflects the gate's orientation + // relative to the robot's approach from below (row 1 end position). Pose2d gate = new Pose2d(gate_X, gate_Y, Math.toRadians(-90)); + // Row-2 intake corridor: start ADDED by +3 (outward to -X), end 6 in. deeper. + // Note the additive offset convention here — the start shifts toward negative X, + // which is toward the field wall (further from the shooting position). Pose2d intake2PoseStart = new Pose2d(INTAKE_START_X + INTAKE2_START_OFFSET_X, INTAKE2_Y, Math.toRadians(180)); Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(180)); + // dodgeGate: +14 in. in X and -4 in. in Y from row-2 end, heading = 140°. + // The -4 in. Y (toward start) combined with +14 in. X creates a diagonal arc that + // steers the robot away from the gate before the final spline toward the shooting pose. Pose2d dodgeGate = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset + 14, INTAKE2_Y - 4, Math.toRadians(140)); + // Row-3 intake corridor: start ADDED by +5, same end depth as row 2. Pose2d intake3PoseStart = new Pose2d(INTAKE_START_X + INTAKE3_START_OFFSET_X, INTAKE3_Y, Math.toRadians(180)); Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(180)); + // Parking pose at (6, 68, 180°). Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(180)); + // toShoot: Drives from start to the shooting pose while simultaneously setting the + // indexer to slot 2, spinning up the flywheel, and firing after the 1.65 s delay. + // Unlike the motif variants, initializeAuto is called HERE (not in a separate start + // action) because there is no toObelisk step preceding this. Action toShoot = new ParallelAction( + // Single-segment spline-heading trajectory from start (0,0,0°) to shooting pose. drive.actionBuilder(startPose) .strafeToSplineHeading(shootingPose.position, shootingPose.heading) .build(), + // Spin flywheel to target RPM during the drive. botActions.actionStartOuttake(SHOOT_RPM), + // Set the indexer drum to slot 2 (pre-loaded artifact position) during the drive. botActions.initializeAuto(Indexer.IndexerState.two), new SequentialAction( + // Wait 1.65 s for robot to arrive and flywheel to reach SHOOT_RPM. new SleepAction(timeUntilStartOuttake), + // Fire the first artifact — no drum rotation, just open the gate. botActions.actionQuickOuttake() ) ); + // intake1: Feedback-driven sweep of row 1 (Y ≈ 50 in.) at 30 in/s. Action intake1 = botActions.actionIntakeThreeFeedback(shootingPose, intake1PoseStart, intake1PoseEnd, drive, maxIntakeDrivingVel); + // goToGate: Optional route through the gate before returning to shoot after row 1. + // Defined but currently unused (commented out in the run sequence). Action goToGate = drive.actionBuilder(intake1PoseEnd) .strafeToLinearHeading(gate.position, gate.heading) + // Pause at the gate for gateWaitTime = 1.5 s if this action is ever enabled. .waitSeconds(gateWaitTime) .build(); + // backToShoot1: Returns from row-1 end directly to the shooting pose and fires + // the second artifact. No dodge needed for row 1 (it's below the gate obstacle). Action backToShoot1 = new ParallelAction( + // Direct spline-heading return from row-1 end to shooting pose. drive.actionBuilder(intake1PoseEnd) // goToGate .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Re-spin the flywheel (it coasted during the intake sweep). botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // Same 1.65 s wait — short drive back from row 1. new SleepAction(timeUntilStartOuttake), + // Fire the second artifact immediately on arrival (no color rotation). botActions.actionQuickOuttake() ) ); + // intake2: Feedback-driven sweep of row 2 (Y ≈ 76.5 in.) at 30 in/s. Action intake2 = botActions.actionIntakeThreeFeedback(shootingPose, intake2PoseStart, intake2PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot2: Two-segment return from row-2 end that arcs around the gate obstacle + // via dodgeGate (140°), then spline-heads to the shooting pose. Fires artifact 3. + // +0.5 s extra wait to account for the longer two-segment path. Action backToShoot2 = new ParallelAction( drive.actionBuilder(intake2PoseEnd) + // Arc through dodgeGate to clear the gate (angled at 140°, -4 in. Y). .strafeToSplineHeading(dodgeGate.position, dodgeGate.heading) + // Then spline-heading into the shooting pose. .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel with the two-segment drive. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // 2.15 s (1.65 + 0.5) — extra buffer for the longer two-segment return. new SleepAction(timeUntilStartOuttake + 0.5), + // Fire the third artifact. botActions.actionQuickOuttake() ) ); @@ -145,44 +320,59 @@ public void runOpMode() { ) );*/ + // intake3: Feedback-driven sweep of row 3 (Y ≈ 96 in.) at 30 in/s. Action intake3 = botActions.actionIntakeThreeFeedback(shootingPose, intake3PoseStart, intake3PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot3: Direct spline-heading return from row-3 end to the shooting pose. + // Row 3 is far enough from the gate (Y=96 vs. gate Y=60) that no dodge is needed. + // +1.0 s extra for the longest return drive of the routine. Action backToShoot3 = new ParallelAction( + // Single-segment spline-heading return. drive.actionBuilder(intake3PoseEnd) .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // 2.65 s (1.65 + 1.0) — longest wait for the most distant return trip. new SleepAction(timeUntilStartOuttake + 1.0), + // Fire the fourth and final artifact. botActions.actionQuickOuttake() ) ); + // toPark: Final spline from the shooting pose to the parking zone for bonus points. Action toPark = drive.actionBuilder(shootingPose) .strafeToSplineHeading( - parkPose.position, - parkPose.heading + parkPose.position, // Target: (6, 68) + parkPose.heading // Re-orient to 180° ) .build(); + // Wait for the Drive Station "Play" button. waitForStart(); + // Exit if emergency-stopped before the match. if (isStopRequested()) return; + // Execute the full routine. runBlocking holds the thread until all actions complete. Actions.runBlocking( + // Outer ParallelAction: two concurrent threads. new ParallelAction( + // Thread 1 — Periodic: telemetry and sensor maintenance. botActions.actionPeriodic(), + // Thread 2 — Main sequential mission. new SequentialAction( - toShoot, - intake1, - /*goToGate,*/ - backToShoot1, - intake2, - backToShoot2, - intake3, - backToShoot3, - toPark + toShoot, // Drive to shoot; init indexer + flywheel; fire artifact 1 + intake1, // Sweep row 1 (Y ≈ 50 in.) + /*goToGate,*/ // (Disabled) Optional gate approach + backToShoot1, // Return to shoot; fire artifact 2 + intake2, // Sweep row 2 (Y ≈ 76.5 in.) + backToShoot2, // Dodge gate; return to shoot; fire artifact 3 + intake3, // Sweep row 3 (Y ≈ 96 in.) + backToShoot3, // Return to shoot; fire artifact 4 + toPark // Drive to parking zone ) ) ); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif.java index 2bb682aa17be..0849f20d1da5 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif.java @@ -17,80 +17,357 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * FasterRedCloseMotif — Red Alliance, Close-Side Autonomous with Motif (Obelisk) Scanning + * + *

Alliance: Red  |  Starting Position: Close side (robot begins near + * the red driver's station, facing the field interior at 180°).

+ * + *

Mission Overview

+ *

This OpMode executes a fully automated 30-second autonomous routine: + *

    + *
  1. Initializes the indexer drum and spins up the flywheel to {@link #SHOOT_RPM} RPM.
  2. + *
  3. Drives to the shooting position via a two-step splined trajectory, while the + * Limelight camera reads the Obelisk AprilTag (the "Motif") in the background. + * The tag encodes which color artifact (GREEN or PURPLE) belongs at each scoring + * structure on the field.
  4. + *
  5. Rotates the 3-slot indexer drum ({@code rotateToMotifColorBeforeOuttake}) to align + * the correct-colored artifact with the shooter, then fires the first preloaded artifact.
  6. + *
  7. Performs three intake cycles at Y ≈ 52, 76, and 96 inches from start, collecting + * field artifacts and returning to the shooting pose to fire a color-matched artifact + * after each cycle.
  8. + *
  9. Parks in the designated zone for the parking bonus.
  10. + *
+ *

+ * + *

"Motif" Variant vs. No-Motif

+ *

The "Motif" variants call {@code rotateToMotifColorBeforeOuttake} before every shot. + * The Limelight camera continuously decodes the Obelisk AprilTag ID ({@code getObeliskId}), + * which tells the robot which color is required at each scoring slot. The indexer drum + * then rotates to present that color before the actuator gate opens. This scores the right + * color automatically rather than relying on manual pre-loading arrangement.

+ * + *

Key Tuning Differences from {@link FasterRedClose} (No-Motif)

+ * + * + *

Coordinate System

+ *

All positions are in inches relative to the robot's start pose (0, 0, 180°). + * Positive Y runs away from the starting wall toward the far side of the field. + * Positive X runs toward the red alliance wall (rightward when facing from the red side). + * Headings are measured counter-clockwise from positive X; the robot starts at 180° + * (facing toward the field interior / negative-X direction).

+ * + *

FTC Dashboard Tuning

+ *

{@code @Config} exposes every {@code public static} field to the FTC Dashboard web + * interface, enabling live position, heading, RPM, and timing adjustments without + * recompiling the code.

+ * + * @see FasterRedClose Non-motif variant for the red close side + * @see FasterBlueCloseMotif Blue alliance mirror of this autonomous + * @see BotActions Factory for all subsystem Road Runner actions + * @see Hardware Initializes drivetrain, indexer, intake, shooter, and camera + */ @Config @Autonomous(name = "Faster Red Auto With Motif", group = "Autonomous") public class FasterRedCloseMotif extends LinearOpMode { + /** + * Maximum translational velocity (inches per second) while the robot drives through the + * intake corridors. Set conservatively at 15 in/s (vs. 30 for non-motif) so the intake + * rollers have longer contact time with each artifact, reducing missed pickups and + * minimizing odometry slip that could corrupt the pose estimate. + */ public static double maxIntakeDrivingVel = 15; + /** + * X-coordinate (inches) of the Obelisk (Motif) AprilTag structure. + * The obelisk is the field element whose AprilTag encodes the color pattern the robot + * must score. Although the explicit drive-to-obelisk step is commented out in this + * variant, the pose is preserved for reference and potential re-enablement. + */ public static double OBELISK_X = 8; + + /** + * Y-coordinate (inches) of the Obelisk (Motif) structure. + * See {@link #OBELISK_X} for full context. + */ public static double OBELISK_Y = 36; + + /** + * Heading (degrees) the robot should face while viewing the Obelisk AprilTag. + * At -60° the robot has rotated clockwise so its Limelight camera has an optimal + * sightline to the tag mounted on the obelisk structure. + */ public static double OBELISK_HEADING_DEG = -60; + /** + * X-coordinate (inches) of the primary shooting position. + * Chosen to give a clear firing lane toward all scoring targets while remaining + * clear of the intake corridors and gate obstacle. + */ public static double SHOOT_X = 17; + + /** + * Y-coordinate (inches) of the primary shooting position. + * At Y = 40 the robot is roughly mid-field on the red alliance side with a clean + * line of sight to the scoring structures. + */ public static double SHOOT_Y = 40; + + /** + * Heading (degrees) of the robot at the shooting position. + * At -135° the robot is turned far enough clockwise that the shooter (and the + * co-located Limelight camera) can see both the scoring targets and the Obelisk + * AprilTag simultaneously, making color decisions at shot time. + */ public static double SHOOT_HEADING_DEG = -135; + + /** + * Small additional heading offset (degrees) applied to {@link #SHOOT_HEADING_DEG} for + * all shots after the first. The +2° counter-clockwise nudge compensates for accumulated + * localization drift between shots and fine-tunes aim after the robot has moved through + * multiple intake cycles. The inline comment "// -2" marks the prior value that was + * changed, preserved for tuning history context. + */ public static double SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT = 2; // -2 + /** + * X-coordinate (inches) of the intake sweep start position for all rows. + * The robot begins its intake sweep from this X value and drives toward negative X + * (toward the field wall on the red side), sweeping artifacts into the intake rollers. + */ public static double INTAKE_START_X = 12; + + /** + * Inward X offset (inches) subtracted from {@link #INTAKE_START_X} for the second + * intake row. Shifting the start point inward by 3 in. avoids re-collecting artifacts + * from row 1 and ensures better alignment with the row-2 artifact positions. + */ public static double INTAKE2_START_OFFSET_X = 3.0; + + /** + * Inward X offset (inches) subtracted from {@link #INTAKE_START_X} for the third + * intake row. The larger 5 in. shift accounts for the deeper field position of row-3 + * artifacts relative to the robot's normal start-of-sweep location. + */ public static double INTAKE3_START_OFFSET_X = 5.0; + + /** + * X-coordinate (inches) at the end of the first intake sweep. + * The robot drives from {@link #INTAKE_START_X} to this negative value, sweeping + * across the full width of the intake corridor to collect all available row-1 artifacts. + * The negative value places the robot near the far field wall on the red side. + */ public static double INTAKE_END_X = -16.5; + + /** + * Additional X distance (inches) subtracted from {@link #INTAKE_END_X} for the second + * and third intake rows. Artifacts in rows 2 and 3 are positioned further into the field + * (more negative X), so the robot must travel deeper to collect them all. + */ public static double intake_END_2And3_XOffset = 8; + /** + * Y-coordinate (inches) of the first intake row of field artifacts. + * At ~52 inches from start, this is the nearest row to the robot's launch position, + * making it the fastest to reach after the first shot. + */ public static double INTAKE1_Y = 52; + + /** + * Y-coordinate (inches) of the second intake row of field artifacts. + * At ~76 inches, this is approximately mid-field. The return path from here requires + * navigating around the gate obstacle via the {@code dodgeGate} waypoint. + */ public static double INTAKE2_Y = 76; + + /** + * Y-coordinate (inches) of the third intake row of field artifacts. + * At ~96 inches, this is the farthest row from the start — near the far end of the field. + * Despite the distance, the return path is simpler because the robot bypasses the gate. + */ public static double INTAKE3_Y = 96; + /** + * X-coordinate (inches) of the field gate obstacle. + * The gate is a physical field element situated between the intake rows and the shooting + * position. It is referenced when computing the {@code dodgeGate} intermediate waypoint + * so the robot steers around it on the return from rows 2 and 3. + */ public static double gate_X = -12; + + /** + * Y-coordinate (inches) of the field gate obstacle. + * At ~60 inches the gate sits roughly between rows 1 and 2, squarely in the most direct + * return path from rows 2 and 3 back to the shooting position. + */ public static double gate_Y = 60; + /** + * X-coordinate (inches) of the final parking position. + * After all four shots are fired, the robot drives here to earn the autonomous parking + * bonus points. + */ public static double PARK_X = 6; + + /** + * Y-coordinate (inches) of the final parking position. + * At Y = 68, the parking zone lies between the row-1 intake corridor and the gate obstacle. + */ public static double PARK_Y = 68; + /** + * Target flywheel speed in RPM for the shooter motor. + * 3580 RPM provides sufficient projectile velocity from the shooting position to reach + * the scoring targets. This is higher than the no-motif variant (3240) to maintain + * consistent shot power regardless of which drum slot is aligned — different drum + * positions slightly alter the loading geometry and thus the effective launch angle. + */ public static int SHOOT_RPM = 3580; + /** + * Time in seconds to wait after a parallel drive+spinup action begins before triggering + * the outtake (firing) sequence. This delay serves three purposes: + * + * The actual sleep before firing varies per shot: + * + */ public static double timeUntilStartOuttake = 2.5; // Time until you start the outtake action, which still includes the wait for actuator // bunch of compensations for bad rr // has quick outtake and quick intake + + /** + * Main autonomous routine. All Road Runner {@link Action} objects (trajectories and + * subsystem command sequences) are constructed up front before {@code waitForStart}, + * pre-compiling the spline math so there is no computation delay once the match begins. + * After start, {@code Actions.runBlocking} executes the entire choreography. + * + *

Full Execution Order: + *

    + *
  1. {@code start} — Set indexer to slot "two" and spin flywheel to target RPM.
  2. + *
  3. {@code toShoot} — Two-step spline to shooting pose; rotate indexer for slot 0; + * fire the first preloaded artifact.
  4. + *
  5. {@code intake1} — Sweep row 1 (Y ≈ 52 in.) collecting artifacts.
  6. + *
  7. {@code backToShoot1} — Return to shooting pose; rotate indexer for slot 1; + * fire second artifact.
  8. + *
  9. {@code intake2} — Sweep row 2 (Y ≈ 76 in.) collecting artifacts.
  10. + *
  11. {@code backToShoot2} — Navigate around gate via dodgeGate waypoint; rotate + * indexer for slot 2; fire third artifact.
  12. + *
  13. {@code intake3} — Sweep row 3 (Y ≈ 96 in.) collecting artifacts.
  14. + *
  15. {@code backToShoot3} — Return to shooting pose; rotate indexer for slot 3; + * fire fourth artifact.
  16. + *
  17. {@code toPark} — Drive to parking zone.
  18. + *
+ * Throughout the entire run, {@code actionScanObelisk} and {@code actionPeriodic} execute + * as persistent parallel threads, keeping the Limelight scanning and handling background + * telemetry and sensor updates.

+ */ @Override public void runOpMode() { + // The robot starts at the field origin facing 180° (toward the field interior / + // negative-X direction). Road Runner uses this seed pose for all downstream + // trajectory calculations — every position in the route is relative to this origin. Pose2d startPose = new Pose2d(0, 0, Math.toRadians(180)); + // Initialize all hardware: mecanum drive (with Road Runner two-wheel + IMU localizer), + // 3-slot indexer drum, intake roller, flywheel shooter, actuator gate, and Limelight. + // Passing startPose seeds the drive's odometry estimator immediately at construction. Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); + + // BotActions wraps every subsystem command into a Road Runner Action object so they + // can be composed with ParallelAction / SequentialAction / SleepAction, making the + // entire autonomous a single declarative action tree. BotActions botActions = hardware.actions; + // MecanumDrive provides trajectory following via Road Runner's motion profile system. + // Its actionBuilder API compiles B-spline paths into timed velocity/acceleration profiles. MecanumDrive drive = hardware.mecanumDrive; + // The obelisk pose records where the field's Motif AprilTag structure is located. + // Even though the robot no longer drives to this pose explicitly (toObelisk is disabled), + // this value is still used conceptually to understand the tag's field position. Pose2d obeliskPose = new Pose2d( - OBELISK_X, - OBELISK_Y, - Math.toRadians(OBELISK_HEADING_DEG) + OBELISK_X, // ~8 in. along X + OBELISK_Y, // ~36 in. along Y + Math.toRadians(OBELISK_HEADING_DEG) // -60° — angled toward the tag ); + // The shooting pose is the robot's position and orientation when firing artifacts. + // At (17, 40, -135°) the robot is on the near-center area of the red side, rotated + // so the shooter and Limelight camera both point toward the scoring structures. Pose2d shootingPose = new Pose2d( - SHOOT_X, - SHOOT_Y, - Math.toRadians(SHOOT_HEADING_DEG) + SHOOT_X, // ~17 in. along X + SHOOT_Y, // ~40 in. along Y + Math.toRadians(SHOOT_HEADING_DEG) // -135° ); + // Row-1 intake corridor: robot sweeps from X=12 to X=-16.5 at Y=52, + // collecting the nearest row of field artifacts with the intake rollers running. + // Heading = 180° keeps the intake facing the correct direction throughout. Pose2d intake1PoseStart = new Pose2d(INTAKE_START_X, INTAKE1_Y, Math.toRadians(180)); - Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(180)); + Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(180)); + + // The gate pose captures the position of the physical gate obstacle at (−12, 60, 90°). + // While the goToGate action is currently unused (commented out), the gate position + // directly informs the dodgeGate waypoint below. Pose2d gate = new Pose2d(gate_X, gate_Y, Math.toRadians(90)); + // Row-2 intake corridor: shifted 3 in. inward at the start (to avoid re-collecting + // row-1 artifacts) and 8 in. deeper at the end (row-2 artifacts are further into the field). Pose2d intake2PoseStart = new Pose2d(INTAKE_START_X - INTAKE2_START_OFFSET_X, INTAKE2_Y, Math.toRadians(180)); - Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(180)); + Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(180)); + + // dodgeGate is an intermediate waypoint on the return trip from rows 2 and 3. + // By steering +12 in. in X and +4 in. in Y from the row-2 end position (and angling + // to 160°), the robot curves around the gate obstacle before the final turn toward + // the shooting pose. Without this waypoint the straight path would collide with the gate. Pose2d dodgeGate = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset + 12, INTAKE2_Y + 4, Math.toRadians(160)); + // Row-3 intake corridor: furthest from start (Y ≈ 96 in.), largest start offset (5 in.) + // to target the deepest artifacts. End X is same depth as row 2. Pose2d intake3PoseStart = new Pose2d(INTAKE_START_X - INTAKE3_START_OFFSET_X, INTAKE3_Y, Math.toRadians(180)); - Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(180)); + Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(180)); + // Parking pose: the robot ends here after firing all four artifacts to claim + // the autonomous parking bonus. Heading = 180° matches the field's parking zone orientation. Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(180)); + // --- PRE-COMPILED ACTION DEFINITIONS --- + // All actions are constructed here before waitForStart so that spline math is resolved + // and hardware references are bound before the match timer begins. + + // start: Runs before any driving — sets the indexer drum to position "two" (the slot + // loaded with the first artifact to fire) and starts the flywheel spinning toward + // target RPM so it reaches full speed by the time the robot arrives at the shooting pose. + // Note: toObelisk is commented out; obelisk scanning is handled by the persistent outer thread. // Remove actionStartOuttake in toShoot when adding this Action start = new ParallelAction( + // Rotate the indexer drum to the "two" position (pre-loaded artifact slot). botActions.initializeAuto(Indexer.IndexerState.two), + // Begin spinning the flywheel motor toward SHOOT_RPM immediately. botActions.actionStartOuttake(SHOOT_RPM) ); @@ -107,46 +384,93 @@ public void runOpMode() { ); */ + // toShoot: Drives the robot from start to the shooting position while simultaneously + // spinning the flywheel and timing the indexer rotation + first shot. + // The two-step spline trajectory uses an intermediate waypoint (SHOOT_X-9, SHOOT_Y-14) + // to create a smooth curved approach into the final -135° heading — a direct one-step + // path would produce a sharper, less reliable turn near the shooting pose. Action toShoot = new ParallelAction( + // Two-segment spline path: first arc to intermediate point, then curve to shooting pose. + // The intermediate point (8 in. short, 14 in. behind) shapes a smooth entry arc. drive.actionBuilder(startPose) // obeliskPose .strafeToSplineHeading(new Vector2d(shootingPose.position.x - 9, shootingPose.position.y - 14), SHOOT_HEADING_DEG) .strafeToSplineHeading(shootingPose.position, shootingPose.heading) .build(), + // Maintain flywheel at target RPM throughout the drive (runs concurrently). botActions.actionStartOuttake(SHOOT_RPM), - new SequentialAction( - new SleepAction(timeUntilStartOuttake - 0.5), - botActions.rotateToMotifColorBeforeOuttake(0, botActions::getObeliskId, 2), - new SleepAction(0.5), - botActions.actionQuickOuttake() - ) + // Sequential outtake preparation — timed to execute as the robot settles at the pose: + new SequentialAction( + // Sleep 2.0 s (= timeUntilStartOuttake - 0.5) to let the robot reach the + // shooting pose and the flywheel stabilize near target RPM. + new SleepAction(timeUntilStartOuttake - 0.5), + // Rotate the indexer drum to the color required for scoring slot 0. + // getObeliskId supplies the AprilTag ID decoded by the Limelight camera. + // Category 2 means the action will wait for the drum to confirm its position. + botActions.rotateToMotifColorBeforeOuttake(0, botActions::getObeliskId, 2), + // 0.5 s settle time after drum rotation before the gate opens — + // ensures the drum is fully seated before the artifact is launched. + new SleepAction(0.5), + // Open the actuator gate; the spinning flywheel propels the artifact toward + // the scoring target. "Quick" outtake minimizes the open time to avoid + // accidentally ejecting the next artifact in the drum. + botActions.actionQuickOuttake() + ) ); + // intake1: Full feedback-driven intake sequence for row 1 (Y ≈ 52 in.). + // actionIntakeThreeFeedback drives the robot along the intake corridor at maxIntakeDrivingVel + // (15 in/s) while running the intake rollers. It uses sensor feedback (likely color sensor + // or motor current) to detect when each of the three indexer drum slots has been filled, + // stopping or reversing as needed to ensure all three are loaded before returning. Action intake1 = botActions.actionIntakeThreeFeedback(shootingPose, intake1PoseStart, intake1PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot1: Returns from the row-1 end position to the shooting pose and fires + // the second artifact (indexer slot 1). Row 1 is close enough that a single-segment + // spline suffices — no gate-avoidance waypoint is needed. Action backToShoot1 = new ParallelAction( + // Direct spline-heading path from row-1 end to the shooting pose. + // The +2° heading offset (SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT) compensates + // for any slight drift that accumulated during the first intake cycle. drive.actionBuilder(intake1PoseEnd) // goToGate .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), //botActions.actionSetIntakeReverse(), + // Spin the flywheel up in parallel with the drive so it is ready on arrival. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // Sleep 1.5 s (= timeUntilStartOuttake - 1) — shorter than the first shot + // because this return trip is brief and the flywheel stays partially spun up. new SleepAction(timeUntilStartOuttake - 1), + // Rotate drum to the color required for scoring slot 1. botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 2), + // 1 s settle time after drum rotation for mechanical stabilization. new SleepAction(1), + // Fire the second artifact. botActions.actionQuickOuttake(), + // Set intake to passive mode (rollers off, not reversing) to prevent + // accidentally ingesting field elements on the next transit. botActions.actionSetIntakePassive() ) ); + // backToShoot2Spline: ALTERNATIVE return trajectory from row 2, kept as a reference. + // Uses full cubic spline interpolation (splineTo + splineToSplineHeading) instead of + // the active strafeTo + strafeToSplineHeading approach. Spline paths are smoother and + // faster but harder to tune reliably around the gate obstacle; the strafeTo variant + // in backToShoot2 below is the current active choice for consistent gate avoidance. + // Note: the indexer slot index here is 1 (same as backToShoot1), which appears to be + // a copy-paste artifact — the active backToShoot2 correctly uses slot index 2. Action backToShoot2Spline = new ParallelAction( drive.actionBuilder(intake2PoseEnd) //.setTangent(Math.toRadians() + // Spline through the dodgeGate waypoint (arcing around the gate obstacle). .splineTo(dodgeGate.position, dodgeGate.heading) + // Then spline-with-heading interpolation directly into the shooting pose. .splineToSplineHeading( new Pose2d( shootingPose.position, @@ -157,10 +481,12 @@ public void runOpMode() { .build(), //botActions.actionSetIntakeReverse(), + // Flywheel spinup running concurrently with the drive. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( new SleepAction(timeUntilStartOuttake - 1), + // Note: uses slot 1 — likely a copy-paste error from backToShoot1. botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 2), new SleepAction(1), botActions.actionQuickOuttake(), @@ -168,71 +494,117 @@ public void runOpMode() { ) ); + // intake2: Full feedback-driven intake sequence for row 2 (Y ≈ 76 in.). + // Same mechanism as intake1 but targeting the mid-field artifact positions. Action intake2 = botActions.actionIntakeThreeFeedback(shootingPose, intake2PoseStart, intake2PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot2: Returns from the row-2 end position to the shooting pose, navigating + // around the gate obstacle, and fires the third artifact (indexer slot 2). + // Uses a two-segment path: first strafeTo the dodgeGate waypoint (which arcs the robot + // away from the gate), then strafeToSplineHeading into the shooting pose. Action backToShoot2 = new ParallelAction( drive.actionBuilder(intake2PoseEnd) + // Move to dodgeGate first to safely clear the gate obstacle before + // heading toward the shooting position. .strafeTo(dodgeGate.position) + // Then spline-heading into the final shooting pose with heading offset. .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), //botActions.actionSetIntakeReverse(), + // Flywheel spinup in parallel with the two-segment drive. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( + // 1.5 s wait — same as backToShoot1; the two-segment path takes similar time. new SleepAction(timeUntilStartOuttake - 1), + // Rotate drum to the color required for scoring slot 2. botActions.rotateToMotifColorBeforeOuttake(2, botActions::getObeliskId, 2), new SleepAction(1), + // Fire the third artifact. botActions.actionQuickOuttake(), botActions.actionSetIntakePassive() ) ); + // intake3: Full feedback-driven intake sequence for row 3 (Y ≈ 96 in.), the farthest + // intake position. Same feedback mechanism as previous rows. Action intake3 = botActions.actionIntakeThreeFeedback(shootingPose, intake3PoseStart, intake3PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot3: Returns from the row-3 end position to the shooting pose and fires + // the fourth and final artifact (indexer slot 3). Row 3's Y position (96 in.) is far + // enough from the gate that a direct single-segment spline return is unobstructed — + // no dodgeGate waypoint is needed here. Action backToShoot3 = new ParallelAction( + // Single-segment spline-heading return from row-3 end to shooting pose. drive.actionBuilder(intake3PoseEnd) .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), //botActions.actionSetIntakeReverse(), + // Flywheel spinup in parallel. botActions.actionStartOuttake(SHOOT_RPM), new SequentialAction( new SleepAction(timeUntilStartOuttake - 1), + // Rotate drum to the color required for scoring slot 3 (final shot). botActions.rotateToMotifColorBeforeOuttake(3, botActions::getObeliskId, 2), new SleepAction(1), + // Fire the fourth and final artifact. botActions.actionQuickOuttake(), botActions.actionSetIntakePassive() ) ); + // toPark: Terminal spline from the shooting pose to the parking zone. + // The robot ends the autonomous period here to secure the parking bonus points. + // Heading interpolates from the current shooting angle back to 180° (field-interior facing). Action toPark = drive.actionBuilder(shootingPose) .strafeToSplineHeading( - parkPose.position, - parkPose.heading + parkPose.position, // Drive to (6, 68) — the parking zone coordinates + parkPose.heading // Re-orient to 180° in the parking zone ) .build(); + // Block here until the Drive Station operator presses the Play (▶) button. waitForStart(); + // If the OpMode was stopped before it could run (emergency stop before match start), + // exit immediately without executing any robot motion. if (isStopRequested()) return; + // Execute the entire autonomous choreography. runBlocking keeps the OpMode thread + // alive until every action in the tree finishes or the OpMode is externally stopped. Actions.runBlocking( + // Outer ParallelAction launches three concurrent execution threads: new ParallelAction( + // Thread 1 — Obelisk scanner: continuously reads the AprilTag on the + // Obelisk via the Limelight camera. Running this as a persistent parallel + // thread (rather than a one-shot action) ensures the latest tag ID is always + // available when rotateToMotifColorBeforeOuttake polls getObeliskId. botActions.actionScanObelisk(), + + // Thread 2 — Periodic maintenance: updates telemetry, refreshes sensors, + // and keeps any subsystem state machines ticking every loop iteration. botActions.actionPeriodic(), + + // Thread 3 — Main sequential routine: the robot's step-by-step mission. new SequentialAction( + // Instantly re-seed the localizer with the known start pose. + // This zeroes out any drift that occurred between hardware + // initialization and the actual match start (e.g., the robot + // being pushed slightly during last-second field placement). new InstantAction(() -> drive.localizer.setPose(startPose)), - start, - //toObelisk, - toShoot, - intake1, - backToShoot1, - intake2, - backToShoot2, - intake3, - backToShoot3, - toPark + + start, // Set indexer to slot 2, spin flywheel to RPM + //toObelisk, // (Disabled) Explicit drive to obelisk for scan + toShoot, // Two-step spline to shooting pose; fire artifact 1 + intake1, // Sweep row 1 (Y ≈ 52 in.); fill indexer slots + backToShoot1, // Return to shoot; fire artifact 2 (slot 1) + intake2, // Sweep row 2 (Y ≈ 76 in.); refill indexer + backToShoot2, // Dodge gate; return to shoot; fire artifact 3 (slot 2) + intake3, // Sweep row 3 (Y ≈ 96 in.); refill indexer + backToShoot3, // Return to shoot; fire artifact 4 (slot 3) + toPark // Drive to parking zone for bonus points ) ) ); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif15.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif15.java index 819cef981325..811d37447600 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif15.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotif15.java @@ -16,86 +16,309 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * FasterRedCloseMotif15 — Red Alliance, Close-Side Autonomous with Motif Scanning, "15-Ball" Tuning + * + *

Alliance: Red  |  Starting Position: Close side, facing + * field-forward at 0° (positive-X direction).

+ * + *

Mission Overview

+ *

This is a high-throughput variant of the red close-side motif autonomous, tuned to + * score as many artifacts as possible (up to 15 balls across all three intake rows) within + * the 30-second autonomous period: + *

    + *
  1. Initializes the indexer drum to slot "two" and spins the flywheel to + * {@link #SHOOT_RPM} RPM.
  2. + *
  3. Drives directly to the shooting position and fires the first pre-loaded artifact + * immediately after a {@link #timeUntilStartOuttake} delay (no color rotation needed + * for the first shot — see note below).
  4. + *
  5. Simultaneously scans the Obelisk AprilTag during the initial drive so color data + * is ready by the time subsequent shots are needed.
  6. + *
  7. Performs three intake sweeps (rows at Y ≈ 48.5, 76, and 96 in.) and returns to + * shoot after each, rotating the indexer drum to the motif-correct color before + * each subsequent shot (slots 1, 2, 3).
  8. + *
  9. Parks in the designated zone.
  10. + *
+ *

+ * + *

Key Differences from {@link FasterRedCloseMotif} (Standard Motif)

+ * + * + *

Coordinate System

+ *

All positions in inches relative to start pose (0, 0, 0°). The robot faces positive-X + * (field-forward) at the start. Positive Y runs perpendicular toward the far field wall. + * The shooting position is at (17, 40) with a -140° heading.

+ * + * @see FasterRedCloseMotif Standard red close motif autonomous (slower, more accurate) + * @see FasterRedClose Red close autonomous with no motif color matching + * @see BotActions Subsystem action factories + * @see Hardware Hardware initialization and subsystem wiring + */ @Config @Autonomous(name = "Faster Red Auto With Motif 15 Ball", group = "Autonomous") public class FasterRedCloseMotif15 extends LinearOpMode { + /** + * Maximum translational velocity (in/s) while driving through intake corridors. + * Set to 30 in/s (double the standard motif variant) to maximize cycle speed and + * collect the most artifacts possible within the 30-second autonomous window. + * The trade-off is slightly reduced pickup reliability compared to slower speeds. + */ public static double maxIntakeDrivingVel = 30; + /** + * X-coordinate (inches) of the Obelisk (Motif) AprilTag structure. + * Used in the {@code toObelisk} action that is currently commented out in the sequence, + * and as context for the Limelight scan embedded inside {@code toShoot}. + */ public static double OBELISK_X = 8; + + /** + * Y-coordinate (inches) of the Obelisk (Motif) structure. + * See {@link #OBELISK_X} for context. + */ public static double OBELISK_Y = 36; + + /** + * Heading (degrees) the robot faces while pointing at the Obelisk AprilTag. + * At -60° the Limelight camera has an optimal sightline to the tag. + */ public static double OBELISK_HEADING_DEG = -60; + /** + * X-coordinate (inches) of the shooting position. + * Same as the standard motif variant; positioned for a clear shot to all scoring targets. + */ public static double SHOOT_X = 17; + + /** + * Y-coordinate (inches) of the shooting position. + */ public static double SHOOT_Y = 40; + + /** + * Heading (degrees) of the robot at the shooting position. + * At -140° (slightly less aggressive than the standard motif's -135°), tuned to balance + * aim angle and the trajectory approach smoothness for this faster configuration. + */ public static double SHOOT_HEADING_DEG = -140; + + /** + * Small heading offset (degrees) added to {@link #SHOOT_HEADING_DEG} for all shots after + * the first. +2° nudges the robot slightly counter-clockwise to compensate for accumulated + * localization drift across multiple intake-and-return cycles. + */ public static double SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT = 2; // -2 + /** + * X-coordinate (inches) of the intake sweep start for all rows. + * The robot begins sweeping from this point and drives toward negative X to collect artifacts. + */ public static double INTAKE_START_X = 12; + + /** + * Inward X offset (inches) subtracted from {@link #INTAKE_START_X} for row 2. + * Prevents re-collecting row-1 artifacts by shifting the start point inward. + */ public static double INTAKE2_START_OFFSET_X = 3.0; + + /** + * Inward X offset (inches) subtracted from {@link #INTAKE_START_X} for row 3. + * Larger shift for the deepest artifact row. + */ public static double INTAKE3_START_OFFSET_X = 5.0; + + /** + * X-coordinate (inches) at the end of the first intake sweep. + * Shallower than the standard motif variant (-13.5 vs. -16.5), reflecting this tuning's + * field/placement configuration or a narrower artifact spread. + */ public static double INTAKE_END_X = -13.5; + + /** + * Additional depth (inches) subtracted from {@link #INTAKE_END_X} for rows 2 and 3. + * Artifacts in later rows are deeper into the field and require a longer sweep. + */ public static double intake_END_2And3_XOffset = 8; + /** + * Y-coordinate (inches) of the first intake row. + * At 48.5 in. this is slightly closer to start than the standard variant (52 in.), + * shaving a small amount of transit time for the faster 15-ball tuning. + */ public static double INTAKE1_Y = 48.5; + + /** + * Y-coordinate (inches) of the second intake row (mid-field). + */ public static double INTAKE2_Y = 76; + + /** + * Y-coordinate (inches) of the third intake row (near far wall). + */ public static double INTAKE3_Y = 96; + /** + * X-coordinate (inches) of the field gate obstacle used in path planning. + * Referenced by the optional {@code goToGate} action and the {@code dodgeGate} waypoint. + */ public static double gate_X = -12; + + /** + * Y-coordinate (inches) of the field gate obstacle. + */ public static double gate_Y = 60; + /** + * Time in seconds the robot waits at the gate position if the {@code goToGate} action is + * enabled. Currently not used in the active sequence (goToGate is commented out), but + * preserved for testing alternate gate-approach strategies. + */ public static double gateWaitTime = 1.5; + /** + * X-coordinate (inches) of the final parking position. + */ public static double PARK_X = 6; + + /** + * Y-coordinate (inches) of the final parking position. + */ public static double PARK_Y = 68; + /** + * Target flywheel speed in RPM. + * 3580 RPM provides the power needed to reliably reach the scoring targets regardless + * of drum rotation position, same as the standard motif variant. + */ public static int SHOOT_RPM = 3580; + /** + * Base delay (seconds) before triggering the outtake sequence inside each parallel action. + * At 3.0 s this is longer than the standard motif's 2.5 s because this variant drives + * directly to the shooting pose in a single segment (no intermediate waypoint), and the + * single-segment trajectory to (17, 40, -140°) takes slightly more time from (0, 0, 0°). + * Subsequent shots add additional time: backToShoot1 uses {@code +1}, backToShoot2 uses + * {@code +2}, and backToShoot3 uses {@code +1} to handle the longer return paths. + */ public static double timeUntilStartOuttake = 3.0; // Time until you start the outtake action, which still includes the wait for actuator // bunch of compensations for bad rr // has quick outtake and quick intake + + /** + * Main autonomous routine. Pre-compiles all trajectories and action sequences before + * {@code waitForStart}, then runs the full 15-ball routine via {@code Actions.runBlocking}. + * + *

Execution Order: + *

    + *
  1. {@code start} — Initialize indexer to slot 2 and spin flywheel.
  2. + *
  3. {@code toShoot} — Drive to shooting pose (single-segment spline); scan obelisk + * in parallel; fire first artifact immediately after the 3.0 s delay.
  4. + *
  5. {@code intake1} — Sweep row 1 (Y ≈ 48.5 in.) at 30 in/s.
  6. + *
  7. {@code backToShoot1} — Return to shoot; rotate for slot 1; fire artifact 2.
  8. + *
  9. {@code intake2} — Sweep row 2 (Y ≈ 76 in.).
  10. + *
  11. {@code backToShoot2} — Navigate around gate; rotate for slot 2; fire artifact 3.
  12. + *
  13. {@code intake3} — Sweep row 3 (Y ≈ 96 in.).
  14. + *
  15. {@code backToShoot3} — Return; rotate for slot 3; fire artifact 4.
  16. + *
  17. {@code toPark} — Drive to parking zone.
  18. + *
+ * {@code actionPeriodic} runs as a persistent parallel thread throughout.

+ */ @Override public void runOpMode() { + // Robot starts at origin facing 0° (positive-X direction, toward the field interior + // from the red close side in this placement configuration). Pose2d startPose = new Pose2d(0, 0, Math.toRadians(0)); + // Initialize all robot hardware subsystems (drive, indexer, intake, flywheel, camera). Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); + + // BotActions provides composable Road Runner Action objects for every subsystem command. BotActions botActions = hardware.actions; + // MecanumDrive handles mecanum-wheel trajectory following with Road Runner's motion profiles. MecanumDrive drive = hardware.mecanumDrive; + // Obelisk pose for reference and the disabled toObelisk action. + // The scan is embedded in toShoot for this variant instead. Pose2d obeliskPose = new Pose2d( OBELISK_X, OBELISK_Y, Math.toRadians(OBELISK_HEADING_DEG) ); + // Shooting pose: (17, 40, -140°). The slightly shallower heading (-140° vs -135°) + // was tuned to improve trajectory smoothness for the direct single-segment approach. Pose2d shootingPose = new Pose2d( SHOOT_X, SHOOT_Y, Math.toRadians(SHOOT_HEADING_DEG) ); + // Row-1 intake corridor (Y ≈ 48.5 in.): sweeps from X=12 to X=-13.5 facing 180°. Pose2d intake1PoseStart = new Pose2d(INTAKE_START_X, INTAKE1_Y, Math.toRadians(180)); Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(180)); + + // Gate obstacle reference pose (used in the optional goToGate action below). Pose2d gate = new Pose2d(gate_X, gate_Y, Math.toRadians(90)); + // Row-2 intake corridor (Y ≈ 76 in.): start shifted 3 in. inward, end 8 in. deeper. Pose2d intake2PoseStart = new Pose2d(INTAKE_START_X - INTAKE2_START_OFFSET_X, INTAKE2_Y, Math.toRadians(180)); Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(180)); + + // dodgeGate: intermediate waypoint to steer around the gate on the row-2 return. + // +12 in. in X and +4 in. in Y from the row-2 end, heading = 180° (straight return). + // The 180° heading (vs. 160° in the standard motif) reflects the different trajectory + // shape needed when approaching the shooting pose from this starting heading. Pose2d dodgeGate = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset + 12, INTAKE2_Y + 4, Math.toRadians(180)); + // Row-3 intake corridor (Y ≈ 96 in.): deepest position, 5 in. inward start offset. Pose2d intake3PoseStart = new Pose2d(INTAKE_START_X - INTAKE3_START_OFFSET_X, INTAKE3_Y, Math.toRadians(180)); Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(180)); + // Parking pose at (6, 68, 180°) — end position for the autonomous parking bonus. Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(180)); + // start: Pre-match initialization — set indexer to slot 2 and begin flywheel spinup. + // These run in parallel so the flywheel reaches RPM while the indexer positions itself. // Remove actionStartOuttake in toShoot when adding this Action start = new ParallelAction( + // Rotate the indexer drum to the pre-loaded "two" position. botActions.initializeAuto(Indexer.IndexerState.two), + // Start spinning the flywheel toward SHOOT_RPM immediately. botActions.actionStartOuttake(SHOOT_RPM) ); // remove soon + // toObelisk: Alternative first step that drives to the obelisk for a direct tag scan. + // Currently disabled — the scan is now embedded inside toShoot via actionScanObelisk. + // Kept as a reference for restoring the explicit obelisk visit if needed. Action toObelisk = new ParallelAction( drive.actionBuilder(startPose) .strafeToSplineHeading(obeliskPose.position, obeliskPose.heading) @@ -106,55 +329,93 @@ public void runOpMode() { botActions.actionScanObelisk() ); + // toShoot: Single-segment spline to the shooting pose while scanning the obelisk and + // spinning up the flywheel. Unlike the standard motif variant, this fires the first + // artifact WITHOUT a color rotation (the rotation call is commented out with the note + // "no need in 12 ball or more" — at high cycle counts, skipping the first rotation + // saves time that is more valuable than color-matching the pre-loaded artifact). Action toShoot = new ParallelAction( + // Direct single-segment spline-heading path from start (0,0,0°) to shooting pose. + // No intermediate waypoint is needed here because starting at 0° allows a smooth + // single arc into the -140° shooting heading. drive.actionBuilder(startPose) // obeliskPose .strafeToSplineHeading(shootingPose.position, shootingPose.heading) .build(), + // Actively scan the obelisk AprilTag during the drive so color data is ready + // for slots 1, 2, 3 which DO use rotateToMotifColorBeforeOuttake. botActions.actionScanObelisk(), // With new cam pose + // Maintain flywheel at target RPM during transit. botActions.actionStartOuttake(SHOOT_RPM), //botActions.rotateToMotifColorBeforeOuttake(0, botActions::getObeliskId, 0), // - no need in 12 ball or more new SequentialAction( + // Wait 3.0 s for the robot to arrive and the flywheel to reach full speed. new SleepAction(timeUntilStartOuttake), + // Fire the first (pre-loaded) artifact without color matching. botActions.actionQuickOuttake() ) ); + // intake1: Feedback-driven sweep of row 1 (Y ≈ 48.5 in.) at 30 in/s. Action intake1 = botActions.actionIntakeThreeFeedback(shootingPose, intake1PoseStart, intake1PoseEnd, drive, maxIntakeDrivingVel); + // goToGate: Optional route through the gate obstacle before returning to shoot after row 1. + // Drives to the gate at (−12, 60, 90°), waits gateWaitTime = 1.5 s there, then the + // caller would need to add the return-to-shoot segment. Currently commented out in the + // main sequence in favour of the direct backToShoot1 return. Action goToGate = drive.actionBuilder(intake1PoseEnd) .strafeToLinearHeading(gate.position, gate.heading) + // Pause at the gate position for the configured dwell time. .waitSeconds(gateWaitTime) .build(); + // backToShoot1: Returns from row-1 end to the shooting pose and fires the second artifact. + // Uses category 0 rotation (faster, no wait for confirmation) to save time. + // The +4 s total wait (timeUntilStartOuttake + 1 = 4.0 s) gives extra buffer for + // the longer flywheel re-spinup after being idle during the intake sweep. Action backToShoot1 = new ParallelAction( + // Direct spline-heading return from row-1 end to shooting pose with heading offset. drive.actionBuilder(intake1PoseEnd) // goToGate .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Spin flywheel back up to target RPM while driving. botActions.actionStartOuttake(SHOOT_RPM), + // Rotate drum to the motif-correct color for slot 1 (category 0 = no wait). botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 0), new SequentialAction( + // Wait 4.0 s (timeUntilStartOuttake + 1) for arrival and flywheel stabilization. new SleepAction(timeUntilStartOuttake + 1), + // Fire the second artifact. botActions.actionQuickOuttake() ) ); + // intake2: Feedback-driven sweep of row 2 (Y ≈ 76 in.) at 30 in/s. Action intake2 = botActions.actionIntakeThreeFeedback(shootingPose, intake2PoseStart, intake2PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot2: Returns from row-2 end, navigating around the gate via dodgeGate, + // and fires the third artifact. The +2 s extra wait (5.0 s total) accounts for the + // longer two-segment path from the deepest row-2 position. Action backToShoot2 = new ParallelAction( drive.actionBuilder(intake2PoseEnd) + // Strafe to the dodgeGate intermediate waypoint to clear the gate obstacle. .strafeTo(dodgeGate.position) + // Then spline-heading into the shooting pose. .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup concurrently with the two-segment drive. botActions.actionStartOuttake(SHOOT_RPM), + // Rotate drum to motif-correct color for slot 2 (category 0). botActions.rotateToMotifColorBeforeOuttake(2, botActions::getObeliskId, 0), new SequentialAction( + // Wait 5.0 s (timeUntilStartOuttake + 2) — extra time for the longer path. new SleepAction(timeUntilStartOuttake + 2.0), + // Fire the third artifact. botActions.actionQuickOuttake() ) ); @@ -178,48 +439,69 @@ public void runOpMode() { ) );*/ + // intake3: Feedback-driven sweep of row 3 (Y ≈ 96 in.) at 30 in/s. Action intake3 = botActions.actionIntakeThreeFeedback(shootingPose, intake3PoseStart, intake3PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot3: Returns from row-3 end directly to the shooting pose (no gate dodge + // needed — row 3 is far enough from the gate that the direct spline avoids it). + // Fires the fourth and final artifact. Action backToShoot3 = new ParallelAction( + // Direct single-segment spline-heading return from row-3 end. drive.actionBuilder(intake3PoseEnd) .strafeToSplineHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel. botActions.actionStartOuttake(SHOOT_RPM), + // Rotate drum to motif-correct color for slot 3 (category 0). botActions.rotateToMotifColorBeforeOuttake(3, botActions::getObeliskId, 0), new SequentialAction( + // Wait 4.0 s (timeUntilStartOuttake + 1) for arrival and spinup. new SleepAction(timeUntilStartOuttake + 1.0), + // Fire the fourth and final artifact. botActions.actionQuickOuttake() ) ); + // toPark: Final trajectory from the shooting pose to the parking zone. Action toPark = drive.actionBuilder(shootingPose) .strafeToSplineHeading( - parkPose.position, - parkPose.heading + parkPose.position, // Target: (6, 68) + parkPose.heading // Re-orient to 180° ) .build(); + // Block until the Drive Station starts the match. waitForStart(); + // Exit immediately if the OpMode was stopped before execution (e.g., emergency stop). if (isStopRequested()) return; + // Execute the full autonomous routine. runBlocking drives the OpMode thread until + // every action finishes or the OpMode is externally terminated. Actions.runBlocking( + // Outer ParallelAction provides two concurrent threads: new ParallelAction( + // Thread 1 — Periodic maintenance: telemetry updates and sensor refreshes. botActions.actionPeriodic(), + + // Thread 2 — Main sequential mission routine. new SequentialAction( + // Re-seed the Road Runner localizer with the exact known start pose, + // correcting any pose drift between hardware init and match start. new InstantAction(() -> drive.localizer.setPose(startPose)), - start, - //toObelisk, - toShoot, - intake1, - /*goToGate,*/ - backToShoot1, - intake2, - backToShoot2, - intake3, - backToShoot3, - toPark + + start, // Set indexer slot 2, spin flywheel + //toObelisk, // (Disabled) Drive to obelisk explicitly + toShoot, // Drive to shooting pose; scan obelisk; fire artifact 1 + intake1, // Sweep row 1 (Y ≈ 48.5 in.) + /*goToGate,*/ // (Disabled) Optional gate approach from row 1 + backToShoot1, // Return to shoot; fire artifact 2 (slot 1) + intake2, // Sweep row 2 (Y ≈ 76 in.) + backToShoot2, // Dodge gate; return to shoot; fire artifact 3 (slot 2) + intake3, // Sweep row 3 (Y ≈ 96 in.) + backToShoot3, // Return to shoot; fire artifact 4 (slot 3) + toPark // Drive to parking zone ) ) ); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotifLinear.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotifLinear.java index ac92fb87aa32..7e4c36c1d5a9 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotifLinear.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/opmodes/FasterRedCloseMotifLinear.java @@ -16,144 +16,359 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * FasterRedCloseMotifLinear — Red Alliance, Close-Side Autonomous with Motif Scanning, + * using Linear Heading Interpolation throughout. + * + *

Alliance: Red  |  Starting Position: Close side, facing + * field-forward at 0° (positive-X direction).

+ * + *

What "Linear Heading" Means

+ *

Road Runner offers two heading interpolation strategies along a trajectory: + *

+ * This variant uses {@code strafeToLinearHeading} (and {@code strafeToLinearHeading}) for + * all driving segments, making heading transitions more deterministic. It was likely + * developed to test whether linear interpolation improves shooting accuracy by ensuring the + * robot reliably ends at the exact heading needed for the shot.

+ * + *

Unique Characteristics vs. Other Motif Variants

+ * + * + *

Execution Sequence

+ *

start pose (0,0,0°) → toObelisk → toShoot (from obeliskPose) → [intake/shoot ×3] → park

+ * + * @see FasterRedCloseMotif Standard motif variant (spline heading, 180° start) + * @see FasterRedCloseMotif15 High-throughput motif variant (no explicit obelisk visit) + * @see BotActions Subsystem action factory + * @see Hardware Hardware initialization + */ @Config @Autonomous(name = "Faster Red Auto With Motif Linear Heading", group = "Autonomous") public class FasterRedCloseMotifLinear extends LinearOpMode { + /** + * Maximum translational velocity (in/s) during intake driving segments. + * 30 in/s matches the 15-ball variant — prioritising speed for high cycle count. + */ public static double maxIntakeDrivingVel = 30; + /** + * X-coordinate (inches) of the Obelisk (Motif) structure. + * This is actually visited in the active sequence ({@code toObelisk} is not commented out + * in the {@code runBlocking} call), so the robot physically drives here first. + */ public static double OBELISK_X = 8; + + /** Y-coordinate (inches) of the Obelisk structure. */ public static double OBELISK_Y = 36; + + /** + * Heading (degrees) the robot faces at the obelisk to optimally view the AprilTag + * with the Limelight camera. + */ public static double OBELISK_HEADING_DEG = -60; + /** X-coordinate (inches) of the shooting position. */ public static double SHOOT_X = 17; + + /** Y-coordinate (inches) of the shooting position. */ public static double SHOOT_Y = 40; + + /** + * Heading (degrees) of the robot at the shooting position. + * At -145° this is the steepest clockwise rotation of any red motif variant, tuned + * specifically for the approach from the obelisk pose using linear heading interpolation. + */ public static double SHOOT_HEADING_DEG = -145; + + /** + * Heading offset (degrees) applied after the first shot. Set to 0° — linear heading + * interpolation provides sufficient accuracy that no correction offset is needed. + */ public static double SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT = 0; // -2 + /** X-coordinate (inches) of the intake sweep start. */ public static double INTAKE_START_X = 12; + + /** Inward X offset (inches) for the row-2 sweep start position. */ public static double INTAKE2_START_OFFSET_X = 3.0; + + /** Inward X offset (inches) for the row-3 sweep start position. */ public static double INTAKE3_START_OFFSET_X = 5.0; + + /** + * X-coordinate (inches) at the end of the row-1 intake sweep. + * Same depth as the 15-ball variant (-13.5 in.) rather than the standard motif (-16.5 in.). + */ public static double INTAKE_END_X = -13.5; + + /** Additional depth (inches) for the row-2 and row-3 intake end positions. */ public static double intake_END_2And3_XOffset = 8; + /** Y-coordinate (inches) of the first intake row (same as 15-ball at 48.5 in.). */ public static double INTAKE1_Y = 48.5; + + /** Y-coordinate (inches) of the second intake row. */ public static double INTAKE2_Y = 76; + + /** Y-coordinate (inches) of the third intake row. */ public static double INTAKE3_Y = 96; + /** X-coordinate (inches) of the gate obstacle reference. */ public static double gate_X = -12; + + /** Y-coordinate (inches) of the gate obstacle reference. */ public static double gate_Y = 60; + /** + * Dwell time (seconds) at the gate position when {@code goToGate} is used. + * Currently the action is commented out in the active sequence. + */ public static double gateWaitTime = 1.5; + /** X-coordinate (inches) of the final parking position. */ public static double PARK_X = 6; + + /** Y-coordinate (inches) of the final parking position. */ public static double PARK_Y = 68; + /** + * Target flywheel speed in RPM. + * 3480 RPM — slightly lower than the other motif variants (3580) and fine-tuned for + * the specific geometry of this variant's linear-heading approach to the shooting pose. + */ public static int SHOOT_RPM = 3480; + /** + * Base delay (seconds) before triggering the outtake sequence. + * At 1.0 s this is the shortest of all motif variants. Because the robot visits the + * obelisk first and the flywheel is already spinning during that transit, it is closer + * to target RPM when {@code toShoot} begins — less spinup time is needed. + * Subsequent shots: backToShoot1/3 use {@code +0} (same 1.0 s); backToShoot2 uses {@code +1.0}. + */ public static double timeUntilStartOuttake = 1.0; // Time until you start the outtake action, which still includes the wait for actuator // bunch of compensations for bad rr // has quick outtake and quick intake + + /** + * Main autonomous routine. Pre-compiles all trajectories and actions, then executes + * the full sequence after {@code waitForStart}. + * + *

Key difference from other variants: This routine actually runs {@code toObelisk} + * as the first driving step (it is not commented out in the {@code SequentialAction} inside + * {@code runBlocking}). The {@code toShoot} action therefore starts its trajectory from + * {@code obeliskPose} rather than {@code startPose}.

+ * + *

Execution Order: + *

    + *
  1. {@code toObelisk} — Drive to the obelisk; scan the AprilTag; initialize indexer; + * spin up flywheel. (start action is commented out — init happens here.)
  2. + *
  3. {@code toShoot} — Linear-heading drive from obelisk to shooting pose; rotate + * indexer for slot 0; fire first artifact.
  4. + *
  5. {@code intake1} — Sweep row 1 at 30 in/s.
  6. + *
  7. {@code backToShoot1} — Linear-heading return; rotate for slot 1; fire artifact 2.
  8. + *
  9. {@code intake2} — Sweep row 2.
  10. + *
  11. {@code backToShoot2} — Two-segment linear-heading return via dodgeGate; + * rotate for slot 2; fire artifact 3.
  12. + *
  13. {@code intake3} — Sweep row 3.
  14. + *
  15. {@code backToShoot3} — Linear-heading return; rotate for slot 3; fire artifact 4.
  16. + *
  17. {@code toPark} — Drive to parking zone.
  18. + *
+ * {@code actionPeriodic} runs as a persistent parallel thread throughout.

+ */ @Override public void runOpMode() { + // Robot starts at the field origin facing 0° (positive-X direction, field-forward). Pose2d startPose = new Pose2d(0, 0, Math.toRadians(0)); + // Initialize all hardware subsystems. Hardware hardware = new Hardware(hardwareMap, telemetry, this, startPose); + + // BotActions wraps subsystem commands into composable Road Runner Actions. BotActions botActions = hardware.actions; + // MecanumDrive with Road Runner trajectory following. MecanumDrive drive = hardware.mecanumDrive; + // Obelisk pose: the robot actually drives here in this variant (toObelisk is active) + // to let the Limelight get a clear read of the field's Motif AprilTag before shooting. Pose2d obeliskPose = new Pose2d( OBELISK_X, OBELISK_Y, Math.toRadians(OBELISK_HEADING_DEG) ); + // Shooting pose: (17, 40, -145°). The approach from the obelisk (at ~8, 36, -60°) + // to this pose is handled with a single linear-heading segment in toShoot. Pose2d shootingPose = new Pose2d( SHOOT_X, SHOOT_Y, Math.toRadians(SHOOT_HEADING_DEG) ); + // Row-1 intake corridor: sweeps from X=12 to X=-13.5 at Y=48.5, heading = 180°. Pose2d intake1PoseStart = new Pose2d(INTAKE_START_X, INTAKE1_Y, Math.toRadians(180)); Pose2d intake1PoseEnd = new Pose2d(INTAKE_END_X, INTAKE1_Y, Math.toRadians(180)); + + // Gate obstacle reference: used in the optional goToGate action and for the + // dodgeGate computation below. Pose2d gate = new Pose2d(gate_X, gate_Y, Math.toRadians(90)); + // Row-2 intake corridor: offset start, deeper end. Pose2d intake2PoseStart = new Pose2d(INTAKE_START_X - INTAKE2_START_OFFSET_X, INTAKE2_Y, Math.toRadians(180)); Pose2d intake2PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE2_Y, Math.toRadians(180)); + + // dodgeGate: intermediate waypoint for the row-2 return path. + // +12 in. in X and +4 in. in Y from the row-2 end, maintaining 180° heading + // (a simpler linear dodge compared to the angled approach of other variants). Pose2d dodgeGate = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset + 12, INTAKE2_Y + 4, Math.toRadians(180)); + // Row-3 intake corridor: furthest from start (Y ≈ 96 in.). Pose2d intake3PoseStart = new Pose2d(INTAKE_START_X - INTAKE3_START_OFFSET_X, INTAKE3_Y, Math.toRadians(180)); Pose2d intake3PoseEnd = new Pose2d(INTAKE_END_X - intake_END_2And3_XOffset, INTAKE3_Y, Math.toRadians(180)); + // Parking pose: final destination after all shots are complete. Pose2d parkPose = new Pose2d(PARK_X, PARK_Y, Math.toRadians(180)); + // start: Defined but commented out in the run sequence — initialization is handled + // by toObelisk which includes initializeAuto and actionStartOuttake. // Remove actionStartOuttake in toShoot when adding this Action start = new ParallelAction( + // Set indexer to pre-loaded slot 2. botActions.initializeAuto(Indexer.IndexerState.two), + // Begin flywheel spinup. botActions.actionStartOuttake(SHOOT_RPM) ); // remove soon + // toObelisk: ACTIVE in this variant — the robot drives to the obelisk pose using + // linear heading interpolation. This ensures the Limelight has a clear, stable view + // of the AprilTag before the shooting sequence begins. + // initializeAuto and actionStartOuttake run in parallel with the drive so the indexer + // and flywheel are ready by the time the robot reaches the obelisk. Action toObelisk = new ParallelAction( + // Linear-heading drive from start to the obelisk pose. + // strafeToLinearHeading produces a constant angular rate turn from 0° to -60° + // while following the translational path. drive.actionBuilder(startPose) .strafeToLinearHeading(obeliskPose.position, obeliskPose.heading) .build(), + // Initialize the indexer drum to slot 2 while driving. botActions.initializeAuto(Indexer.IndexerState.two), + // Spin the flywheel to target RPM while driving. botActions.actionStartOuttake(SHOOT_RPM), + // Scan the Obelisk AprilTag during the approach — by the time the robot + // reaches the obelisk and then the shooting pose, the tag ID will be decoded. botActions.actionScanObelisk() ); + // toShoot: Linear-heading drive FROM the obelisk pose TO the shooting pose, + // while rotating the indexer for slot 0 and firing the first artifact. + // Starts from obeliskPose (not startPose) because toObelisk runs before this. Action toShoot = new ParallelAction( + // Single linear-heading segment from obelisk to shooting position. + // Heading linearly interpolates from -60° to -145° over the distance traveled. drive.actionBuilder(obeliskPose) .strafeToLinearHeading(shootingPose.position, shootingPose.heading) .build(), + // Maintain flywheel at target RPM during transit (already at RPM from toObelisk). botActions.actionStartOuttake(SHOOT_RPM), + // Rotate indexer to motif-correct color for slot 0 (category 0 = fast, no wait). botActions.rotateToMotifColorBeforeOuttake(0, botActions::getObeliskId, 0), // - no need in 12 ball or more new SequentialAction( + // Only 1.0 s wait needed since flywheel was already spinning during toObelisk. new SleepAction(timeUntilStartOuttake), + // Fire the first (pre-loaded) artifact. botActions.actionQuickOuttake() ) ); + // intake1: Feedback-driven sweep of row 1 (Y ≈ 48.5 in.) at 30 in/s. Action intake1 = botActions.actionIntakeThreeFeedback(shootingPose, intake1PoseStart, intake1PoseEnd, drive, maxIntakeDrivingVel); + // goToGate: Optional route through the gate obstacle. Defined but not used in the + // active sequence (commented out with /*goToGate,*/ in runBlocking). Action goToGate = drive.actionBuilder(intake1PoseEnd) .strafeToLinearHeading(gate.position, gate.heading) + // Pause at gate for gateWaitTime seconds. .waitSeconds(gateWaitTime) .build(); + // backToShoot1: Linear-heading return from row-1 end to shooting pose. + // Rotates indexer for slot 1 in parallel (category 0), fires the second artifact. Action backToShoot1 = new ParallelAction( + // Linear-heading drive from row-1 end to shooting pose (0° offset = exact heading). drive.actionBuilder(intake1PoseEnd) // goToGate .strafeToLinearHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel. botActions.actionStartOuttake(SHOOT_RPM), + // Rotate drum for slot 1 color (category 0 = no confirmation wait). botActions.rotateToMotifColorBeforeOuttake(1, botActions::getObeliskId, 0), new SequentialAction( + // 1.0 s wait before firing (short since flywheel was kept warm). new SleepAction(timeUntilStartOuttake), + // Fire the second artifact. botActions.actionQuickOuttake() ) ); + // intake2: Feedback-driven sweep of row 2 (Y ≈ 76 in.) at 30 in/s. Action intake2 = botActions.actionIntakeThreeFeedback(shootingPose, intake2PoseStart, intake2PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot2: Two-segment linear-heading return from row-2 end via dodgeGate. + // Uses strafeToLinearHeading for both segments (unique to this linear variant — + // other variants use strafeTo + strafeToSplineHeading for the same dodge maneuver). Action backToShoot2 = new ParallelAction( drive.actionBuilder(intake2PoseEnd) + // Segment 1: Linear-heading drive to dodgeGate to clear the gate obstacle. .strafeToLinearHeading(dodgeGate.position, dodgeGate.heading) + // Segment 2: Linear-heading drive from dodgeGate into the shooting pose. .strafeToLinearHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel with the two-segment drive. botActions.actionStartOuttake(SHOOT_RPM), + // Rotate drum for slot 2 color. botActions.rotateToMotifColorBeforeOuttake(2, botActions::getObeliskId, 0), new SequentialAction( + // +1.0 s extra (2.0 s total) for the longer two-segment path from row 2. new SleepAction(timeUntilStartOuttake + 1.0), + // Fire the third artifact. botActions.actionQuickOuttake() ) ); @@ -177,50 +392,70 @@ public void runOpMode() { ) );*/ + // intake3: Feedback-driven sweep of row 3 (Y ≈ 96 in.) at 30 in/s. Action intake3 = botActions.actionIntakeThreeFeedback(shootingPose, intake3PoseStart, intake3PoseEnd, drive, maxIntakeDrivingVel); + // backToShoot3: Linear-heading return from row-3 end directly to the shooting pose. + // No gate dodge needed — the direct path from Y=96 clears the gate at Y=60. Action backToShoot3 = new ParallelAction( + // Single linear-heading segment from row-3 end to shooting pose. drive.actionBuilder(intake3PoseEnd) .strafeToLinearHeading(shootingPose.position, shootingPose.heading.plus(Math.toRadians(SHOOT_HEADING_OFFSET_AFTER_FIRSTSHOT))) .build(), + // Flywheel spinup in parallel. botActions.actionStartOuttake(SHOOT_RPM), + // Rotate drum for slot 3 color (final shot). botActions.rotateToMotifColorBeforeOuttake(3, botActions::getObeliskId, 0), new SequentialAction( + // +1.0 s extra (2.0 s total) to account for the longer transit from row 3. new SleepAction(timeUntilStartOuttake + 1.0), + // Fire the fourth and final artifact. botActions.actionQuickOuttake() ) ); + // toPark: Linear-heading drive from the shooting pose to the parking zone. Action toPark = drive.actionBuilder(shootingPose) .strafeToLinearHeading( - parkPose.position, - parkPose.heading + parkPose.position, // Target coordinates: (6, 68) + parkPose.heading // Final heading: 180° ) .build(); + // Wait for the Drive Station "Play" button. waitForStart(); + // Exit if emergency-stopped before the match begins. if (isStopRequested()) return; + // Execute the full autonomous sequence, blocking until complete. Actions.runBlocking( + // Outer ParallelAction: two concurrent threads. new ParallelAction( + // Thread 1 — Periodic maintenance: telemetry and sensor updates. botActions.actionPeriodic(), + + // Thread 2 — Main sequential mission routine. new SequentialAction( + // Re-seed the localizer with the exact start pose to eliminate + // any drift accumulated between hardware init and match start. new InstantAction(() -> drive.localizer.setPose(startPose)), - //start, - toObelisk, - toShoot, - intake1, - /*goToGate,*/ - backToShoot1, - intake2, - backToShoot2, - intake3, - backToShoot3, - toPark + + //start, // (Disabled) Separate init step — handled by toObelisk + toObelisk, // Drive to obelisk; init indexer + flywheel; scan tag + toShoot, // Linear-heading drive to shooting pose; fire artifact 1 + intake1, // Sweep row 1 (Y ≈ 48.5 in.) + /*goToGate,*/ // (Disabled) Optional gate approach from row 1 + backToShoot1, // Return to shoot; fire artifact 2 (slot 1) + intake2, // Sweep row 2 (Y ≈ 76 in.) + backToShoot2, // Two-segment dodge-gate return; fire artifact 3 (slot 2) + intake3, // Sweep row 3 (Y ≈ 96 in.) + backToShoot3, // Return to shoot; fire artifact 4 (slot 3) + toPark // Drive to parking zone ) ) ); } } + diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Action.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Action.java index b8d49ddb4d7d..ee25a34c53ae 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Action.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Action.java @@ -3,43 +3,129 @@ import java.util.ArrayList; import java.util.List; +/** + * Action is a lightweight sequential-task runner for use in autonomous + * routines that do not use the full Road Runner Action framework. + * + *

An {@code Action} holds an ordered list of {@link Runnable} steps. + * Calling {@link #run()} executes every step in sequence, blocking until + * all steps complete. + * + *

Instances are constructed exclusively through the nested {@link Builder} + * to enforce a clear, readable construction pattern: + *

+ *   Action myAction = new Action.Builder()
+ *       .add(() -> subsystem.doSomething())
+ *       .moveTo(() -> drive.moveTo(targetPose))
+ *       .add(() -> shooter.fire())
+ *       .build();
+ *   myAction.run();
+ * 
+ * + *

Note: This class predates the Road Runner-based + * {@link com.acmerobotics.roadrunner.Action} pipeline and is used in + * helper utilities that need a simple imperative list of operations. + */ public class Action { + // ----------------------------------------------------------------------- + // Steps + // ----------------------------------------------------------------------- + + /** + * The ordered list of {@link Runnable} steps to execute when {@link #run()} + * is called. Populated only by the {@link Builder}. + */ private final List steps; + // ----------------------------------------------------------------------- + // Constructor (private – use Builder) + // ----------------------------------------------------------------------- + + /** + * Private constructor; only the {@link Builder} may create Action instances. + * + * @param steps the ordered list of steps to execute + */ // Constructor is private; only Builder can build Actions private Action(List steps) { - this.steps = steps; + this.steps = steps; // store the immutable-from-outside step list } + // ----------------------------------------------------------------------- + // Execution + // ----------------------------------------------------------------------- + + /** + * Executes all steps in order, blocking until each step completes. + * + *

Steps are run synchronously on the calling thread; there is no + * timeout or interruption mechanism. + */ // Run all steps in order public void run() { for (Runnable step : steps) { - step.run(); + step.run(); // execute each step sequentially } } + // ----------------------------------------------------------------------- + // Builder + // ----------------------------------------------------------------------- + + /** + * Fluent builder for constructing {@link Action} instances. + * + *

Steps can be added in any order using {@link #add(Runnable)} or + * {@link #moveTo(Runnable)}. Call {@link #build()} to produce the + * immutable {@link Action}. + */ // Builder nested class public static class Builder { + + /** Accumulates steps in insertion order. */ private final List steps = new ArrayList<>(); + /** + * Creates an empty Builder. + */ public Builder() {} + /** + * Appends a custom step to the action sequence. + * + * @param step any {@link Runnable} to execute at this position in the sequence + * @return this Builder (enables method chaining) + */ // Add a custom step public Builder add(Runnable step) { - steps.add(step); - return this; // Allows chaining + steps.add(step); // append to the ordered step list + return this; // return this for chaining } + /** + * Appends a movement step to the action sequence. + * Semantically equivalent to {@link #add(Runnable)}; provided as a + * named convenience to make movement steps visually distinct in code. + * + * @param movement the movement command to execute (e.g., a drive-to-pose call) + * @return this Builder (enables method chaining) + */ // Example: move to a position public Builder moveTo(Runnable movement) { - steps.add(movement); + steps.add(movement); // append the movement command like any other step return this; } + /** + * Finalises the builder and returns the constructed {@link Action}. + * The returned Action's step list is a snapshot of all steps added so far. + * + * @return a new {@link Action} containing all added steps in order + */ // Build the org.firstinspires.ftc.teamcode.auto.utils.Action public Action build() { - return new Action(steps); + return new Action(steps); // construct the Action from the accumulated steps } } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/BotActions.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/BotActions.java index 246bca6a11e9..3814e020c7c7 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/BotActions.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/BotActions.java @@ -26,141 +26,374 @@ import java.util.function.IntSupplier; +/** + * BotActions is the autonomous action factory for the robot. + * + *

Every method in this class returns a Road Runner {@link Action} that + * encapsulates one logical autonomous step (intake a set of artifacts, perform + * an outtake dump, scan the obelisk tag, etc.). The actions are composed + * together in individual auto OpModes using + * {@link SequentialAction} / {@link ParallelAction}. + * + *

The class is annotated with {@link Config} so all {@code public static} + * tuning constants (spin times, powers, RPM scales) are editable via FTC + * Dashboard without re-deploying code. + * + *

All subsystem references are injected through the {@link Hardware} data + * class so that the same hardware objects are shared with the OpMode. + */ @Config public class BotActions { + + // ----------------------------------------------------------------------- + // Injected dependencies + // ----------------------------------------------------------------------- + + /** The running autonomous OpMode; used to check stop/isActive in loop Actions. */ private final LinearOpMode opMode; + + /** FTC telemetry used to log debug data during Action execution. */ private final Telemetry telemetry; + + /** Conveyor/intake roller subsystem reference. */ private final Intake intake; + + /** Three-slot indexer drum subsystem reference. */ private final Indexer indexer; + + /** Flywheel shooter subsystem reference. */ private final Outtake outtake; + + /** Actuator (launch ramp or ball-gate) subsystem reference. */ private final Actuator actuator; + + /** Limelight AprilTag scanner (public so OpModes can call scan/pipeline directly). */ public final AprilTag aprilTag; + + /** Localization-based heading aimer for auto heading lock. */ private final Aimer aprilAimer; + + /** Road Runner drive for trajectory actions and pose estimation. */ private final MecanumDrive drive; - public static double NON_INDEX_SPIN_TIME = 2.0; //seconds of full-power indexer blast + // ----------------------------------------------------------------------- + // Tuning constants (Dashboard-editable) + // ----------------------------------------------------------------------- + + /** Duration (seconds) of the full-power indexer blast during the quick outtake. + * Long enough to eject all three artifacts; shorter = less dead time. */ + public static double NON_INDEX_SPIN_TIME = 2.0; + + /** Raw motor power applied to the indexer drum during the blast phase. + * Higher values push artifacts through faster but may cause jamming. */ public static double FULL_BLAST_POWER = 0.30; + /** Time (seconds) after the start of the intake trajectory before the indexer + * automatically advances to slot 1 to accept the first ball. */ public static double ball1TimeDisp = 0.66; - public static double ball2TimeDisp = 1.10; - public static double timeToIntake = 2.50; + /** Time (seconds) after the start of the intake trajectory before the indexer + * automatically advances to slot 2 to accept the second ball. */ + public static double ball2TimeDisp = 1.10; + + /** Total time (seconds) the robot spends over the intake zone before it starts + * the return trajectory toward the shooting position. */ + public static double timeToIntake = 2.50; + + /** Whether to maintain continuous AprilTag heading lock during an action sequence. + * When {@code true}, {@link #actionPeriodic()} applies heading corrections. */ private boolean continuousAprilTagLock; + + /** Minimum time (ms) between successive artifact acquisitions in the feedback + * intake action. Prevents double-counting a single artifact that briefly + * trips the sensor twice. */ public static double cooldownFeedbackIntake = 0; + + /** Scale factor applied to the target RPM during the quick-spin outtake. + * Values < 1.0 give slightly lower RPM but allow faster spin-up. */ public static double quickspinRpmScale = 0.93; + + /** The most recent heading turn correction from the AprilTag aimer. + * Held between update cycles so correction is not dropped between frames. */ private double lastTurnCorrection; + + /** The most recently detected obelisk AprilTag ID. + * 0 = no detection; 21/22/23 = valid motif tag IDs. */ private int obeliskId = 0; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Constructs a BotActions by extracting all subsystem references from the + * provided {@link Hardware} container. + * + * @param hardware the robot hardware container (all subsystems already built) + * @param telemetry FTC telemetry for debug output during actions + * @param opMode the running autonomous LinearOpMode (for stop checks) + */ public BotActions( Hardware hardware, Telemetry telemetry, LinearOpMode opMode ) { - this.intake = hardware.intake; - this.indexer = hardware.indexer; - this.outtake = hardware.outtake; - this.actuator = hardware.actuator; - this.aprilTag = hardware.aprilTag; + // Unpack each subsystem reference from the hardware container. + this.intake = hardware.intake; + this.indexer = hardware.indexer; + this.outtake = hardware.outtake; + this.actuator = hardware.actuator; + this.aprilTag = hardware.aprilTag; this.aprilAimer = hardware.aprilAimer; - this.drive = hardware.mecanumDrive; + this.drive = hardware.mecanumDrive; this.telemetry = telemetry; - this.opMode = opMode; + this.opMode = opMode; } + // ----------------------------------------------------------------------- + // Outtake actions + // ----------------------------------------------------------------------- + + /** + * Returns an instantaneous {@link Action} that simultaneously enables the + * indexer's auto-outtake mode and sets the shooter to the given RPM + * (scaled by {@link #quickspinRpmScale}). + * + *

Call this before a trajectory to pre-spin the shooter so it is at + * speed by the time the robot reaches the shooting position. + * + * @param rpm the base target RPM for the shooter flywheel + * @return a {@link ParallelAction} that starts indexer auto-outtake and shooter spin-up + */ public Action actionStartOuttake(double rpm) { return new ParallelAction( - new InstantAction(() -> indexer.setAutoOuttaking(true)), - new InstantAction(() -> outtake.set(rpm * quickspinRpmScale)) + new InstantAction(() -> indexer.setAutoOuttaking(true)), // enable auto-outtake mode + new InstantAction(() -> outtake.set(rpm * quickspinRpmScale)) // spin up to scaled RPM ); } + /** + * Returns an {@link Action} that performs a full "quick dump": + * raise the actuator, blast all artifacts out with the indexer, then lower + * and reset everything for the next intake cycle. + * + *

Sequence: + *

    + *
  1. Raise actuator to the "quick" (lower) up position.
  2. + *
  3. Wait 350 ms for the actuator to reach position.
  4. + *
  5. Run the indexer drum at {@link #FULL_BLAST_POWER} for + * {@link #NON_INDEX_SPIN_TIME} seconds.
  6. + *
  7. Stop the drum; disable auto-outtake mode.
  8. + *
  9. Move indexer to slot 1 (so the next intake starts from slot 0).
  10. + *
  11. Stop the shooter; lower the actuator.
  12. + *
  13. Reset all slot colour assignments to EMPTY.
  14. + *
+ * + * @return the quick-dump {@link Action} sequence + */ public Action actionQuickOuttake() { return new SequentialAction( - new InstantAction(actuator::upQuick),// lower up position for quick dump - new SleepAction(.35),// for actuator - new InstantAction(() -> indexer.setIndexerPower(FULL_BLAST_POWER)),// full blast - new SleepAction(NON_INDEX_SPIN_TIME), - new InstantAction(indexer::stopIndexerPower), - new InstantAction(() -> indexer.setAutoOuttaking(false)), - new InstantAction(() -> indexer.moveTo(Indexer.IndexerState.one)), // This means, if you don't move the indexer, the next intaken will enter slot 0 - new InstantAction(outtake::stop), - new InstantAction(actuator::down), - new InstantAction(indexer::initializeColors) + new InstantAction(actuator::upQuick), // 1. raise actuator (lower "up" position for quick dump) + new SleepAction(.35), // 2. wait for actuator to reach position (~350 ms) + new InstantAction(() -> indexer.setIndexerPower(FULL_BLAST_POWER)),// 3. blast drum at full power + new SleepAction(NON_INDEX_SPIN_TIME), // 4. hold blast for configured duration + new InstantAction(indexer::stopIndexerPower), // 5. stop the drum + new InstantAction(() -> indexer.setAutoOuttaking(false)), // 6. disable auto-outtake mode + new InstantAction(() -> indexer.moveTo(Indexer.IndexerState.one)), // 7. home to slot 1 (next intake enters slot 0) + new InstantAction(outtake::stop), // 8. stop shooter + new InstantAction(actuator::down), // 9. lower actuator back to intake position + new InstantAction(indexer::initializeColors) // 10. mark all slots EMPTY ); } + // ----------------------------------------------------------------------- + // Obelisk motif helpers + // ----------------------------------------------------------------------- + + /** + * Returns an instantaneous {@link Action} that rotates the indexer drum to + * the correct starting slot for the motif encoded in the obelisk tag. + * + *

The action reads the obelisk tag ID lazily (at action-run time, not at + * method-call time) via the {@code id} {@link IntSupplier} so the ID is + * available only after {@link #actionScanObelisk()} has completed. + * + *

Algorithm: + *

    + *
  1. Read the tag ID from {@code id}; abort if not 21/22/23.
  2. + *
  3. Apply the per-row colour assignments to the indexer (rotated for + * {@code startingSlot} so the first intaken ball lands in the right + * position).
  4. + *
  5. Convert the obelisk ID to the desired shooting color order.
  6. + *
  7. Search all three indexer slots and rotate to the one where the + * first two slots match the desired order.
  8. + *
+ * + * @param row field row number (0–3) whose colour pattern is being shot + * @param id supplier that returns the current obelisk tag ID at run time + * @param startingSlot the indexer slot index where the first intaken artifact landed + * @return an {@link InstantAction} that rotates the drum to the correct firing position + */ // helpers at the end of the file public Action rotateToMotifColorBeforeOuttake(int row, IntSupplier id, int startingSlot) { return new InstantAction(() -> { - // inside action to read obelisk id when function runs not runtime + // Read the obelisk ID at action-run time (not when this method was called) + // so that a prior scanObelisk action has time to populate it. int tagId = id.getAsInt(); if (tagId != 21 && tagId != 22 && tagId != 23) { + // Not a valid motif tag – log and bail out without moving the drum. telemetry.addData("No obelisk Id 21, 22 or 23, id is", tagId); return; } - // set current color configuration + // Apply the per-row colour assignments, rotated for the starting slot. applyCurrentColorsFromRow(row, startingSlot); - // get desired firing order from obelisk id + // Convert the obelisk ID to the desired artifact firing order. Indexer.ArtifactColor[] desiredOrder = getDesiredShootOrder(tagId); - // values gets an array of the enums + // Search all three indexer slots for the slot whose first two colours + // match the desired firing order. for (Indexer.IndexerState state : Indexer.IndexerState.values()) { telemetry.addData("Started search for index of proper", "color"); if (matchesOrder(state.index, desiredOrder)) { telemetry.addData("Rotated To Motif", "Color"); - // Indexer.IndexerState gotoState = Indexer.IndexerState.values()[(state.index - 1) % Indexer.IndexerState.values().length]; + // Rotate the drum to the matching slot so it fires in the right order. Indexer.IndexerState gotoState = state; - indexer.moveTo(gotoState, true); - return; + indexer.moveTo(gotoState, true); // true = block until rotation complete + return; // stop after the first matching slot is found } } }); } + /** + * Returns an {@link Action} that switches the indexer back to intake mode + * at slot two. Used to reset indexer state between outtake and intake phases. + * + * @return a {@link SequentialAction} that disables auto-outtake, enables intake + * at slot two, and rotates the drum to slot two + */ public Action actionSetSomeShizzle() { return new SequentialAction( - new InstantAction(() -> indexer.setAutoOuttaking(false)), - new InstantAction(() -> indexer.setIntaking(true, Indexer.IndexerState.two)), - new InstantAction(() -> indexer.moveTo(Indexer.IndexerState.two, true)) + new InstantAction(() -> indexer.setAutoOuttaking(false)), // disable auto-outtake + new InstantAction(() -> indexer.setIntaking(true, Indexer.IndexerState.two)), // enable intake starting at slot 2 + new InstantAction(() -> indexer.moveTo(Indexer.IndexerState.two, true)) // physically rotate drum to slot 2 ); } + // ----------------------------------------------------------------------- + // Intake command helpers + // ----------------------------------------------------------------------- + + /** + * Returns an instantaneous {@link Action} that runs the intake in slow + * reverse (used to push partially-captured artifacts back out during reset). + * + * @return an {@link InstantAction} that calls {@code intake.runBackwardsSlow()} + */ public Action actionSetIntakeReverse() { - return new InstantAction(intake::runBackwardsSlow); + return new InstantAction(intake::runBackwardsSlow); // slow reverse to eject partial artifacts } + /** + * Returns an instantaneous {@link Action} that sets the intake to slow + * passive speed (keeps the intake alive without aggressive pulling). + * + * @return an {@link InstantAction} that calls {@code intake.runSlow()} + */ public Action actionSetIntakePassive() { - return new InstantAction(intake::runSlow); + return new InstantAction(intake::runSlow); // passive slow intake } + // ----------------------------------------------------------------------- + // Timed intake trajectory + // ----------------------------------------------------------------------- + + /** + * Returns an {@link Action} that drives the robot to the intake field zone, + * runs the intake during the approach, and advances the indexer at + * pre-calibrated time offsets to accept up to three artifacts. + * + *

The indexer advancement is time-based (not sensor-based); adjust + * {@link #ball1TimeDisp} and {@link #ball2TimeDisp} to match the actual + * spacing between field artifacts. + * + * @param startActionPose the robot pose at which to begin this action + * @param startIntakePose the pose where the intake port aligns with the first artifact + * @param endPose the pose to return to after intake (typically shooting position) + * @param drive the Road Runner drive used to build the trajectory + * @param maxVel maximum translational velocity (in/s) for the return segment + * @return the built trajectory {@link Action} + */ public Action actionIntakeThree(Pose2d startActionPose, Pose2d startIntakePose, Pose2d endPose, MecanumDrive drive, double maxVel) { + // Limit return-segment velocity to maxVel so the robot doesn't overshoot. TranslationalVelConstraint velConstraint = new TranslationalVelConstraint(maxVel); return drive.actionBuilder(startActionPose) - .strafeToSplineHeading(startIntakePose.position, startIntakePose.heading) - .afterTime(0, intake::run) - .afterTime(ball1TimeDisp, () -> indexer.moveTo(indexer.getState().next())) - .afterTime(ball2TimeDisp, () -> indexer.moveTo(indexer.getState().next())) - .afterTime(timeToIntake, intake::runSlow) - .strafeToLinearHeading(endPose.position, endPose.heading, velConstraint) + .strafeToSplineHeading(startIntakePose.position, startIntakePose.heading) // approach intake zone + .afterTime(0, intake::run) // start intake immediately + .afterTime(ball1TimeDisp, () -> indexer.moveTo(indexer.getState().next())) // advance to slot 1 after first artifact time + .afterTime(ball2TimeDisp, () -> indexer.moveTo(indexer.getState().next())) // advance to slot 2 after second artifact time + .afterTime(timeToIntake, intake::runSlow) // slow down intake at end of dwell time + .strafeToLinearHeading(endPose.position, endPose.heading, velConstraint) // return to shooting position .build(); } - - + // ----------------------------------------------------------------------- + // Auto initialization + // ----------------------------------------------------------------------- + + /** + * Returns an {@link Action} that initializes the robot for autonomous: + * clears all indexer slot colours, enables intake auto-advance at the + * specified starting slot, lowers the actuator, and starts passive intake. + * + *

Note: this is a temporary action for testing; in a real auto run the + * initialization is performed as part of {@link #actionQuickOuttake()}. + * + * @param indexerInitializedSlot the indexer slot to start intake at (ensures + * consistent starting position regardless of where + * the drum was left) + * @return a {@link ParallelAction} that resets the indexer, lowers the actuator, + * and starts slow intake simultaneously + */ public Action initializeAuto(Indexer.IndexerState indexerInitializedSlot) { // only temporary for testing, this is done in actionQuickOuttake return new ParallelAction( new SequentialAction( - new InstantAction(() -> indexer.initializeColors(Indexer.ArtifactColor.EMPTY)), - new InstantAction(() -> indexer.setIntaking(true, indexerInitializedSlot)) // just to make it so it starts at a consistent spot + new InstantAction(() -> indexer.initializeColors(Indexer.ArtifactColor.EMPTY)), // mark all slots empty + new InstantAction(() -> indexer.setIntaking(true, indexerInitializedSlot)) // start intake at specified slot ), - new InstantAction(actuator::down), - new InstantAction(intake::runSlow) + new InstantAction(actuator::down), // lower the actuator to intake position + new InstantAction(intake::runSlow) // begin slow passive intake ); } + // ----------------------------------------------------------------------- + // Feedback-based intake + // ----------------------------------------------------------------------- + + /** + * Returns an {@link Action} that drives to the intake zone and indexes + * artifacts using sensor feedback rather than fixed time offsets. + * + *

The robot drives the trajectory (approach → return) while a parallel + * action monitors the indexer sensor. Each time the sensor detects an + * artifact aligned with the intake port (rising edge), the indexer advances + * to the next slot after a cooldown period. The parallel action ends when + * three artifacts have been acquired or the drive trajectory completes. + * + *

Compared to {@link #actionIntakeThree}, this is more robust to + * varying artifact positions because advancement is triggered by detection + * rather than elapsed time. + * + * @param startActionPose starting pose for the trajectory + * @param startIntakePose pose where the robot begins actively intaking + * @param endPose return pose after intake + * @param drive Road Runner drive for trajectory execution + * @param maxVel maximum return velocity (in/s) + * @return a {@link SequentialAction}: start intake → parallel(drive, feedback-index) → slow intake + */ //feedback based version of actionIntakeT public Action actionIntakeThreeFeedback( Pose2d startActionPose, @@ -169,149 +402,224 @@ public Action actionIntakeThreeFeedback( MecanumDrive drive, double maxVel ) { + // Limit return-segment velocity so the robot doesn't overshoot the shoot position. VelConstraint velConstraint = new TranslationalVelConstraint(maxVel); + // Build the drive trajectory: approach intake zone, then return to end pose. Action driveAction = drive.actionBuilder(startActionPose) - .strafeToSplineHeading(startIntakePose.position, startIntakePose.heading) - .strafeToLinearHeading(endPose.position, endPose.heading, velConstraint) + .strafeToSplineHeading(startIntakePose.position, startIntakePose.heading) // approach + .strafeToLinearHeading(endPose.position, endPose.heading, velConstraint) // return .build(); + // Parallel action that manages intake speed and indexer advancement based on sensor. Action manageIntakeAndIndexing = new Action() { - private boolean lastAlignedNonEmpty = false; - private int acquired = 0; - private double timeoutDuration = 5.0; - private final ElapsedTime acquireCooldown = new ElapsedTime(); + private boolean lastAlignedNonEmpty = false; // sensor state from the previous iteration + private int acquired = 0; // number of artifacts acquired so far (max 3) + private double timeoutDuration = 5.0; // (unused) fallback timeout in seconds + private final ElapsedTime acquireCooldown = new ElapsedTime(); // cooldown between advancements @Override public boolean run(@NonNull TelemetryPacket p) { - // stop if opmode ends + // Abort if the OpMode has been stopped (e.g., end of auto period). if (!opMode.opModeIsActive() || opMode.isStopRequested()) { - intake.runSlow(); // or intake.stop() - return false; + intake.runSlow(); // leave intake in slow mode to avoid sharp stop + return false; // signal that this action is done } - // keep indexer logic alive + // Tick the indexer so its internal PD control and sensor reads stay current. indexer.update(); + // Check if an artifact is currently aligned with the intake port AND present. boolean alignedNonEmpty = indexer.artifactPresentAndAligned(); - // intake mode continuously - if (alignedNonEmpty) intake.runSlow(); // Lowkey the play so that once one is intaken another doesn't get stuck until indexer moves - else intake.run(); + // Modulate intake speed: slow down once an artifact is detected to prevent + // the next one from jamming while the drum rotates. + if (alignedNonEmpty) intake.runSlow(); // slow: artifact present, wait for indexer to advance + else intake.run(); // full speed: no artifact yet, keep pulling + // Rising-edge detection: only count when the sensor just became true. + // Also enforce the cooldown so a single artifact can't be counted twice. if (!lastAlignedNonEmpty && alignedNonEmpty && acquireCooldown.milliseconds() > cooldownFeedbackIntake) { - acquired++; - acquireCooldown.reset(); + acquired++; // increment the acquired count + acquireCooldown.reset(); // restart the cooldown timer if (acquired < 3) { + // Advance to the next slot to make room for the next artifact. indexer.moveTo(indexer.getState().next()); } } - lastAlignedNonEmpty = alignedNonEmpty; + lastAlignedNonEmpty = alignedNonEmpty; // store for rising-edge detection next iter - // keep running until we've acquired 3 + // End condition: all three slots are filled. if (acquired >= 3) { - intake.runSlow(); // or intake.stop() - return false; + intake.runSlow(); // transition to slow intake to avoid jamming the third ball + return false; // signal that intake is complete } + // Write debug data to the telemetry packet for the FTC Dashboard. p.put("acquired", acquired); p.put("alignedNonEmpty", alignedNonEmpty); p.put("indexerState", indexer.getState()); - return true; + return true; // still collecting; keep running this iteration } }; + // Wire everything together: start intake, then drive and index in parallel, then slow. return new SequentialAction( - new InstantAction(intake::run), + new InstantAction(intake::run), // start intake before entering the parallel block new ParallelAction( - driveAction, - manageIntakeAndIndexing + driveAction, // drive the intake trajectory + manageIntakeAndIndexing // concurrently manage intake speed & indexer ), - new InstantAction(intake::runSlow) + new InstantAction(intake::runSlow) // after both finish, settle into slow intake ); } + // ----------------------------------------------------------------------- + // Obelisk scan + // ----------------------------------------------------------------------- + + /** + * Returns an {@link Action} that continuously calls the Limelight to scan + * for the obelisk AprilTag and terminates as soon as a valid motif tag + * (ID 21, 22, or 23) is detected. + * + *

The detected ID is stored in {@link #obeliskId} for later retrieval + * via {@link #getObeliskId()} or via the {@link AprilTag#obeliskId} static + * field. + * + *

Note: Running this as an {@link InstantAction} and wrapping with + * a while loop is not ideal (the action blocks the Road Runner scheduler on + * the same thread); this is a known issue noted in the source. + * + * @return an {@link Action} that loops until a valid obelisk tag is found + */ // bad to do instant action and while loop public Action actionScanObelisk() { return new Action() { - private final ElapsedTime timer = new ElapsedTime(); + private final ElapsedTime timer = new ElapsedTime(); // (unused) reserved for timeout @Override public boolean run(@NonNull TelemetryPacket telemetryPacket) { - /*if (!opMode.opModeIsActive() || opMode.isStopRequested()) { - return false; - }*/ - - /*if (timer.seconds() > 8.0) { - telemetry.addLine("Obelisk scan timed out"); - telemetry.update(); - return false; - }*/ - + // Scan the limelight for the obelisk tag on pipeline 2. aprilTag.scanObeliskTag(); - obeliskId = aprilTag.getObeliskId(); + obeliskId = aprilTag.getObeliskId(); // cache locally for retrieval + // Stop scanning once a valid motif tag is found. if (obeliskId == 21 || obeliskId == 22 || obeliskId == 23) - return false; + return false; // valid tag found: action complete - return true; + return true; // still searching: run again next iteration } }; } + // ----------------------------------------------------------------------- + // Periodic background action + // ----------------------------------------------------------------------- + + /** + * Returns a continuous background {@link Action} that runs every loop + * iteration to keep subsystems updated. + * + *

This action is designed to be raced or run in parallel with trajectory + * actions so that the indexer, outtake RPM controller, and pose estimator + * remain current even while Road Runner is executing a path. + * + *

The action terminates when the OpMode is stopped. + * + * @return a looping {@link Action} that ticks pose estimation, outtake, and indexer + */ public Action actionPeriodic() { return new Action() { @Override public boolean run(@NonNull TelemetryPacket telemetryPacket) { + // Stop if the OpMode ends so this action doesn't outlive the OpMode. if (!opMode.opModeIsActive() || opMode.isStopRequested()) { - return false; + return false; // terminate the periodic action } - drive.updatePoseEstimate(); - outtake.periodic(); - indexer.update(); + // Tick all subsystems that need per-loop updates. + drive.updatePoseEstimate(); // integrate odometry/IMU into the pose estimate + outtake.periodic(); // update flywheel RPM measurement and controller + indexer.update(); // run indexer PD control and color sensing - telemetry.addData("obelisk id: ", obeliskId); - telemetry.update(); // could remove later + telemetry.addData("obelisk id: ", obeliskId); // show last obelisk ID on dashboard + telemetry.update(); + // If continuous heading lock is enabled, scan the goal tag and compute correction. if (continuousAprilTagLock) { - aprilTag.scanGoalTag(); + aprilTag.scanGoalTag(); // update bearing from Limelight double bearing = aprilTag.getBearing(); double turn = 0; if (!Double.isNaN(bearing)) { + // Compute turn correction but (currently) not applied to trajectory. turn = aprilAimer.calculateTurnPowerFromBearing(bearing); } - - } - return true; + return true; // still active; run again next iteration } }; } + // ----------------------------------------------------------------------- + // Park action + // ----------------------------------------------------------------------- + + /** + * Returns an {@link Action} representing the end-of-auto parking sequence. + * Currently a placeholder (1-second sleep); future implementation will + * raise vertical slides or drive to the parking zone. + * + * @return a parking {@link Action} (placeholder) + */ public Action actionPark() { // vert slides return new ParallelAction( - new SleepAction(1) + new SleepAction(1) // placeholder: 1-second wait for park position ); } + // ----------------------------------------------------------------------- + // Accessors + // ----------------------------------------------------------------------- + + /** + * Returns the most recently detected obelisk AprilTag ID. + * Valid motif IDs are 21, 22, and 23. Returns 0 if no tag has been detected. + * + * @return the obelisk tag ID, or 0 if not yet detected + */ public int getObeliskId() { - return obeliskId; + return obeliskId; // most recently scanned obelisk AprilTag ID } - + // ----------------------------------------------------------------------- + // Private helpers + // ----------------------------------------------------------------------- + + /** + * Checks whether the indexer slot at {@code stateIndex} and the next slot + * (modulo 3) match the first two colours of {@code desired}. + * + *

Used by {@link #rotateToMotifColorBeforeOuttake} to find the starting + * rotation that aligns with the desired firing order. + * + * @param stateIndex zero-based indexer slot index to check + * @param desired the desired shooting order (must have at least 2 elements) + * @return {@code true} if slot[stateIndex] == desired[0] AND slot[(stateIndex+1)%3] == desired[1] + */ private boolean matchesOrder(int stateIndex, Indexer.ArtifactColor[] desired) { + // Check the first slot against desired[0] and the next slot (wrapping) against desired[1]. return indexer.getColorAt(Indexer.IndexerState.values()[stateIndex % 3]) == desired[0] && indexer.getColorAt(Indexer.IndexerState.values()[(stateIndex + 1) % 3]) == desired[1]; } @@ -319,59 +627,96 @@ private boolean matchesOrder(int stateIndex, Indexer.ArtifactColor[] desired) { // HELPERS + /** + * Applies per-row colour assignments to the indexer, rotated so that the + * first intaken artifact lands in {@code startingSlot}. + * + *

Row-to-colour mapping (based on field artifact placement): + *

+ * + *

The rotation ensures that if the first artifact is intaken into slot + * {@code startingSlot}, the subsequent slots are assigned colours in the + * order they will be collected. + * + * @param row field row number (0–3); determines the colour pattern + * @param startingSlot the indexer slot index where the first intaken artifact + * will land (basically {@code (lastMovedSlot + 1) % 3} in intake mode) + */ // Sets the indexer's color configuration based on a given row, // rotated so that the first intaken ball is placed in startingSlot private void applyCurrentColorsFromRow(int row, int startingSlot /* basically (the last moved to slot + 1) % 3 [in intaking mode]*/) { - Indexer.ArtifactColor[] intakeOrder; + Indexer.ArtifactColor[] intakeOrder; // colour order for this row (index 0 = first intaken) + // Map the row number to the known artifact color sequence for that row. switch (row) { - case 1: // P P G + case 1: // Row 1 artifact pattern: Purple, Purple, Green intakeOrder = new Indexer.ArtifactColor[] {Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN}; break; - case 2: // P G P + case 2: // Row 2 artifact pattern: Purple, Green, Purple intakeOrder = new Indexer.ArtifactColor[] {Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE}; break; case 0: - case 3: // G P P + case 3: // Row 0 or 3 artifact pattern: Green, Purple, Purple intakeOrder = new Indexer.ArtifactColor[] {Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE}; break; default: - return; + return; // unknown row: do not change the indexer colour state } - // Rotate array so the first intaken ball lands in startingSlot + // Rotate the colour array so the first intaken artifact lands in startingSlot. + // rotated[startingSlot + i] = intakeOrder[i] (modulo 3) Indexer.ArtifactColor[] rotated = new Indexer.ArtifactColor[3]; for (int i = 0; i < 3; i++) { - rotated[(startingSlot + i) % 3] = intakeOrder[i]; + rotated[(startingSlot + i) % 3] = intakeOrder[i]; // place each colour in its rotated slot } + // Apply the rotated colour assignments to the indexer's internal colour table. indexer.initializeColors(rotated[0], rotated[1], rotated[2]); } + /** + * Converts an obelisk AprilTag ID to the corresponding desired artifact + * shooting order (which colour should be fired first, second, and third). + * + *

Tag-to-order mapping: + *

+ * + * @param id the obelisk AprilTag ID (should be 21, 22, or 23) + * @return an array of three {@link Indexer.ArtifactColor} values representing + * the desired firing order; empty array if the ID is unrecognised + */ private Indexer.ArtifactColor[] getDesiredShootOrder(int id) { // After the tag ID cases, you want to change the physical rows into shooting that motif Indexer.ArtifactColor[] desiredShootOrder; switch (id) { - case 21: // G -> P -> P - will shoot out in this order + case 21: // Tag 21 → G -> P -> P - will shoot out in this order desiredShootOrder = new Indexer.ArtifactColor[] {Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE}; break; - case 22: // P -> G -> P + case 22: // Tag 22 → P -> G -> P desiredShootOrder = new Indexer.ArtifactColor[] {Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE}; break; - case 23: // P -> P -> G + case 23: // Tag 23 → P -> P -> G desiredShootOrder = new Indexer.ArtifactColor[] {Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN}; break; default: - desiredShootOrder = new Indexer.ArtifactColor[] {}; + desiredShootOrder = new Indexer.ArtifactColor[] {}; // unknown ID: empty order break; } - return desiredShootOrder; + return desiredShootOrder; // return the color order to fire in } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Hardware.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Hardware.java index 1a26bd4948ad..769b2a5707e5 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Hardware.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Hardware.java @@ -10,33 +10,105 @@ import org.firstinspires.ftc.teamcode.subsystems.limelight.AprilTag; import org.firstinspires.ftc.teamcode.subsystems.limelight.Aimer; +/** + * Hardware is a data class that centralises construction and storage of every + * robot subsystem needed during autonomous OpModes. + * + *

Creating a single {@code Hardware} object in the OpMode's {@code runOpMode()} + * method ensures that: + *

+ * + *

All fields are {@code public final} so they can be read directly without + * getters; this keeps autonomous OpMode code concise. + * + *

Typical usage: + *

+ *   Hardware hw = new Hardware(hardwareMap, telemetry, this, startPose);
+ *   hw.actions.initializeAuto(Indexer.IndexerState.zero).run(...);
+ * 
+ */ public class Hardware { + + // ----------------------------------------------------------------------- + // Subsystem fields + // ----------------------------------------------------------------------- + + /** Conveyor/intake roller subsystem. */ public final Intake intake; + + /** Three-slot indexer drum subsystem. */ public final Indexer indexer; + + /** Flywheel shooter (outtake) subsystem, configured in RPM mode. */ public final Outtake outtake; + + /** Actuator (launch ramp or ball-gate) subsystem. */ public final Actuator actuator; + + /** Limelight AprilTag scanner subsystem. */ public final AprilTag aprilTag; + + /** Localization-based heading aimer. */ public final Aimer aprilAimer; + + /** High-level action factory that builds Road Runner Action sequences for + * autonomous routines (intake, outtake, scanning, etc.). */ public final BotActions actions; + + /** Road Runner MecanumDrive used for trajectory execution and pose estimation. */ public final MecanumDrive mecanumDrive; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Constructs all robot subsystems and applies autonomous-specific configuration. + * + *

Autonomous-specific settings applied to the indexer: + *

+ * + * @param hardwareMap the robot's hardware map used to retrieve all devices + * @param telemetry FTC telemetry for the AprilTag subsystem debug output + * @param opMode the running LinearOpMode (passed to BotActions for stop checks) + * @param startPose the robot's field starting pose for Road Runner's localizer + */ public Hardware(HardwareMap hardwareMap, Telemetry telemetry, LinearOpMode opMode, Pose2d startPose) { + // Build Road Runner MecanumDrive at the provided starting pose. mecanumDrive = new MecanumDrive( hardwareMap, - startPose + startPose // initialise localizer with match starting position ); + // Construct each subsystem from the hardware map. intake = new Intake(hardwareMap); indexer = new Indexer(hardwareMap); - indexer.AUTO_ADVANCEMENT_MODE = Indexer.AutoAdvancement.DISABLED; - indexer.ENABLE_FULL_UNKNOWN_SCAN = false; - indexer.SCAN_COLORS = false; - outtake = new Outtake(hardwareMap, Outtake.Mode.RPM); + // Autonomous-specific indexer configuration: + indexer.AUTO_ADVANCEMENT_MODE = Indexer.AutoAdvancement.DISABLED; // explicit control + indexer.ENABLE_FULL_UNKNOWN_SCAN = false; // skip re-scan (colours known in advance) + indexer.SCAN_COLORS = false; // disable live sensor reads to save loop time + + outtake = new Outtake(hardwareMap, Outtake.Mode.RPM); // RPM closed-loop shooter actuator = new Actuator(hardwareMap); aprilTag = new AprilTag(hardwareMap, telemetry); aprilAimer = new Aimer(mecanumDrive); + // Build the action factory last so it can reference all subsystems above. actions = new BotActions(this, telemetry, opMode); } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Paths.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Paths.java index 0070d3778389..239a25a8818b 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Paths.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/auto/utils/Paths.java @@ -5,8 +5,60 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; +/** + * Paths is a utility class that builds Road Runner trajectory sequences for + * full autonomous routines. + * + *

{@link #buildPath} constructs a single chained trajectory that: + *

    + *
  1. Drives to the obelisk scanning position to read the motif tag.
  2. + *
  3. Drives to the shooting position and performs an outtake.
  4. + *
  5. Iterates over every intake row (2 to 0) and all artifact columns within + * each row, collecting up to three artifacts per row.
  6. + *
  7. Drives to the parking zone at the end of the autonomous period.
  8. + *
+ * + *

Many intermediate steps (obelisk scan, shoot with lock, individual intake + * cycles) are currently commented out while the path structure is being tuned. + * These are preserved in comments so they can be re-enabled as subsystems are + * validated. + * + *

Coordinate system: all positions are Road Runner field-frame inches, + * with X pointing right and Y pointing forward. + */ public class Paths { + /** + * Builds the full autonomous path as a single chained Road Runner + * {@link Action} that can be passed directly to + * {@code Actions.runBlocking()}. + * + *

Path outline: + *

    + *
  1. Start at {@code startPose}.
  2. + *
  3. Strafe (spline heading) to the obelisk scan position.
  4. + *
  5. Strafe to the shooting position and optionally perform a shoot action.
  6. + *
  7. For each intake row (2 down to 0): + * + *
  8. + *
  9. Strafe to the park position at {@code outtakeHeading}.
  10. + *
+ * + * @param mecanumDrive the Road Runner drive used to build trajectories + * @param actions action factory for inline action injection (currently unused but reserved) + * @param startPose field pose at which the path begins + * @param shootPos 2-D field position where the robot shoots + * @param parkPos 2-D field position of the parking zone + * @param obeliskScanPos 2-D field position where the Limelight can see the obelisk tag + * @param artifactPositions 3x4 array: index[row][0] = row start, index[row][1..3] = artifact positions + * @param intakeHeading robot heading (radians) while traversing the intake rows + * @param outtakeHeading robot heading (radians) at the shooting and parking positions + * @param obeliskScanHeading robot heading (radians) when scanning the obelisk tag + * @return the complete autonomous trajectory as a single {@link Action} + */ public static Action buildPath( MecanumDrive mecanumDrive, BotActions actions, @@ -19,52 +71,45 @@ public static Action buildPath( double outtakeHeading, double obeliskScanHeading ) { - //actions.initializeColors(Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE); - + // Start building the trajectory chain from the provided starting pose. TrajectoryActionBuilder builder = mecanumDrive.actionBuilder(startPose); - /*builder.stopAndAdd( - new RaceAction( - actions.actionPeriodic(), // keeps updating shooter/indexer - builder.fresh() - .build()) - - );*/ + // Drive to the obelisk scan position so the Limelight can see the motif tag. + // (The scan action is currently commented out while being validated.) builder.strafeToSplineHeading(obeliskScanPos, obeliskScanHeading) - /*.stopAndAdd(actions.actionScanObelisk())*/; + /*.stopAndAdd(actions.actionScanObelisk())*/; // enable when obelisk scan is ready + + // Drive to the shooting position and execute the shoot-with-lock action. + // (Shoot action is commented out; trajectory movement is active.) builder.strafeToSplineHeading(shootPos, outtakeHeading) /*.stopAndAdd(actions.actionShootWithLock(actions.aprilTag.getObeliskId(), 2.0, mecanumDrive))*/; + // Iterate from the farthest intake row (row 2) back to the nearest (row 0). for (int row = 2; row >= 0; row--) { - // Move to the row's starting position for intake + // Move to the starting position of this row at the intake heading. builder.strafeToSplineHeading( - artifactPositions[row][0], - intakeHeading + artifactPositions[row][0], // first waypoint of the row + intakeHeading // heading that points intake toward the artifacts ); + // Traverse each artifact column within the row (columns 1, 2, 3). for (int col = 1; col <= 3; col++) { - builder/*.stopAndAdd(new ParallelAction( - //actions.actionIntakeOneCycle(), - // mecanumDrive.actionBuilder(new Pose2d(artifactPositions[row][0], intakeHeading)) - */.strafeToSplineHeading( - artifactPositions[row][col], - intakeHeading - ) - /*) - )*/; - //builder.afterTime(.5, actions.actionIntakeOneCycle()); + // Strafe to each artifact's field position to collect it. + builder.strafeToSplineHeading( + artifactPositions[row][col], // x,y of this artifact + intakeHeading // keep same intake heading throughout + ); } + // After collecting all artifacts in this row, drive to shoot. + // (Disabled; re-enable when sorted outtake is validated.) //builder.stopAndAdd(actions.actionShootWithLock(actions.aprilTag.getObeliskId(), 2.0, mecanumDrive)); - - // already turns off lock-in mode in action - // builder.stopAndAdd(new InstantAction(() -> continuousAprilTagLock = false)); } + // After all intake rows, drive to the parking zone and stop. return builder - .strafeToSplineHeading(parkPos, outtakeHeading) - //.stopAndAdd(actions.actionPark()) - .build(); + .strafeToSplineHeading(parkPos, outtakeHeading) // drive to park position + //.stopAndAdd(actions.actionPark()) // enable when parking action is ready + .build(); // finalise and return the trajectory action } } - diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Actuator.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Actuator.java index 15a7bbd3b754..61b25d52d57d 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Actuator.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Actuator.java @@ -4,63 +4,197 @@ import com.arcrobotics.ftclib.hardware.SimpleServo; import com.qualcomm.robotcore.hardware.HardwareMap; +/** + * Actuator controls the physical ramp/deflector servo that lifts game elements + * (artifacts) from the indexer into the shooter barrel. + * + *

Three positions are defined: + *

+ * + *

The servo is mapped by the hardware configuration name {@code "actuator"} and + * operates over a 0–360-degree range (FTCLib {@link SimpleServo}). The position + * values below are fractional (0.0–1.0) within that range. + * + *

Annotated with {@link Config} so every {@code public static} field can be + * tuned live in the FTC Dashboard without redeploying. + */ @Config public class Actuator { - public static double DOWN = 0.45; // flush with the floor of platform - public static double UP_QUICK = 0.22; // used for quick (non-indexed) outtake - public static double UP_INDEXED = 0.13; // used for indexed outtake\ - public enum ActuatorState - { + // ----------------------------------------------------------------------- + // Dashboard-tunable servo position constants + // ----------------------------------------------------------------------- + + /** Servo position (fractional 0–1) for the fully-lowered ramp. + * At this position the ramp is flush with the floor of the launch platform, + * allowing artifacts to sit flat while the robot intakes or drives. */ + public static double DOWN = 0.45; + + /** Servo position (fractional 0–1) for the mid-height "quick outtake" ramp. + * Slightly lower than {@link #UP_INDEXED}, used when all artifacts are + * dumped at once via full-power indexer blast rather than precise indexing. */ + public static double UP_QUICK = 0.22; + + /** Servo position (fractional 0–1) for the highest "indexed outtake" ramp. + * Raises the ramp to the steepest angle so a single aligned artifact slides + * directly into the shooter's feed zone during precision firing. */ + public static double UP_INDEXED = 0.13; + + // ----------------------------------------------------------------------- + // State machine enum + // ----------------------------------------------------------------------- + + /** + * Represents the three discrete positions of the actuator servo. + * The state is tracked internally so that callers can query the current + * position without needing to read back hardware values. + */ + public enum ActuatorState { + /** Ramp fully lowered – safe for intake and driving. */ DOWN, + /** Ramp at mid-height – used for quick/non-indexed shooting. */ UP_QUICK, + /** Ramp at maximum height – used for single-artifact indexed shooting. */ UP_INDEXED } + // ----------------------------------------------------------------------- + // Fields + // ----------------------------------------------------------------------- + + /** Tracks the last commanded position of the actuator servo. */ private ActuatorState state; + + /** The physical servo hardware object. Mapped to the name {@code "actuator"} + * with a 0–360-degree range via FTCLib's {@link SimpleServo}. */ private final SimpleServo servo; + + /** Approximate time in seconds for the servo to travel between positions. + * Callers that need to wait for the ramp to reach its target before + * firing can sleep for this duration. */ private double waitTime = 0.5; // seconds + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Initialises the Actuator by fetching the servo from the hardware map. + * + * @param hardwareMap the robot's hardware map, used to look up the servo + * device registered under the name {@code "actuator"} + */ public Actuator(HardwareMap hardwareMap) { + // Create a SimpleServo wrapper that expects a 0–360° operating range. + // FTCLib converts the 0–1 fractional position values to the actual + // PWM range required by the underlying servo hardware. servo = new SimpleServo(hardwareMap, "actuator", 0, 360); } + // ----------------------------------------------------------------------- + // Actuator movement methods + // ----------------------------------------------------------------------- + + /** + * Lowers the ramp to its fully-down position ({@link #DOWN}). + * Should be called after every shooting sequence or during intake so + * artifacts can collect properly. + */ public void down() { - servo.setPosition(DOWN); - state = ActuatorState.DOWN; + servo.setPosition(DOWN); // command servo to the lowered position + state = ActuatorState.DOWN; // update cached state to reflect new position } - //default up is indexed + /** + * Raises the ramp to the default "up" position, which is the indexed + * (highest) angle. Delegates to {@link #upIndexed()}. + * + *

Using indexed as the default ensures that callers who don't care about + * the specific mode always get the most precise shooting angle. + */ public void up() { + // Default up = indexed (most precise / highest angle) upIndexed(); } - //higher position + /** + * Raises the ramp to the indexed outtake position ({@link #UP_INDEXED}). + * This is the steepest angle, used when the indexer is precisely aligned + * so that exactly one artifact enters the shooter barrel. + */ public void upIndexed() { - servo.setPosition(UP_INDEXED); - state = ActuatorState.UP_INDEXED; + servo.setPosition(UP_INDEXED); // command servo to the indexed (highest) position + state = ActuatorState.UP_INDEXED; // update cached state } - //lower position + /** + * Raises the ramp to the quick outtake position ({@link #UP_QUICK}). + * This is a lower angle than {@link #upIndexed()}, used during the + * "quick dump" sequence where the indexer spins at full power to eject + * all three artifacts simultaneously. + */ public void upQuick() { - servo.setPosition(UP_QUICK); - state = ActuatorState.UP_QUICK; + servo.setPosition(UP_QUICK); // command servo to the quick-dump (mid) position + state = ActuatorState.UP_QUICK; // update cached state } + // ----------------------------------------------------------------------- + // Query methods + // ----------------------------------------------------------------------- + + /** + * Returns {@code true} if the ramp is in any raised position (i.e. NOT down). + * Useful for telemetry and guard conditions that check whether the robot is + * ready to shoot. + * + * @return {@code true} if {@link #state} is {@link ActuatorState#UP_QUICK} + * or {@link ActuatorState#UP_INDEXED}; {@code false} if it is + * {@link ActuatorState#DOWN} + */ public boolean isActivated() { + // The actuator is considered "activated" (raised) whenever it is NOT in the DOWN state. return !(state == ActuatorState.DOWN); } + /** + * Returns the current cached {@link ActuatorState} of the ramp. + * + * @return the last position commanded to the actuator servo + */ public ActuatorState getState() { return state; } + /** + * Convenience setter that raises or lowers the ramp based on a boolean flag. + * Calls {@link #up()} when {@code true} (which defaults to indexed mode), + * and {@link #down()} when {@code false}. + * + * @param activate {@code true} to raise the ramp; {@code false} to lower it + */ public void set(boolean activate) { - if (activate) up(); - else down(); + if (activate) up(); // raise ramp (defaults to indexed/highest position) + else down(); // lower ramp to intake/transit position } + /** + * Returns the estimated time in seconds for the servo to complete its + * travel between positions. Callers may use this value in a + * {@link com.acmerobotics.roadrunner.SleepAction} to wait for the ramp + * to physically reach its target before proceeding. + * + * @return servo travel wait time in seconds + */ public double getWaitTime() { - return waitTime; // seconds + return waitTime; // seconds – used externally to gate action sequences on physical completion } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/AntiDefense.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/AntiDefense.java index 8792d183016d..3b0a1b41387a 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/AntiDefense.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/AntiDefense.java @@ -7,41 +7,159 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; +/** + * AntiDefense provides a pose-hold (position lock) capability for the robot. + * + *

When a defending robot makes contact and tries to push this robot off its + * intended position, AntiDefense counteracts the push by computing the error + * between the robot's current pose and a saved reference pose, then commanding + * the drive motors with a proportional correction signal to drive back toward + * that reference. + * + *

Usage: + *

    + *
  1. Call {@link #lock(Pose2d)} when the robot reaches the position it should + * hold (e.g., just before starting the shooting sequence).
  2. + *
  3. Call {@link #update(Pose2d)} every loop iteration to apply the + * correction. If the error is within tolerance the robot holds still; + * otherwise proportional drive powers push it back.
  4. + *
  5. Call {@link #unlock()} to release the pose lock when done.
  6. + *
+ */ public class AntiDefense { + // ----------------------------------------------------------------------- + // Dashboard-tunable gains and tolerances + // ----------------------------------------------------------------------- + + /** Proportional gain for X/Y position correction. + * Multiplied by the XY error vector to produce a drive velocity. + * Increase to push back harder; too high causes oscillation. */ public static double kP = 0.0; - public static double kH = 0.0; //heading gain + + /** Heading (angular) proportional gain. + * Multiplied by the heading error (radians) to produce a yaw correction. */ + public static double kH = 0.0; // heading gain + + /** Maximum XY distance (inches) from the reference pose before correction + * activates. Within this tolerance the robot is considered "on target" + * and no drive output is applied. */ public static double xyTolerance = 0.5; - public static double aTolerance = Math.PI /30; + + /** Maximum heading error (radians) before rotational correction activates. + * π/30 ≈ 6° – small enough to keep the robot well-aimed. */ + public static double aTolerance = Math.PI / 30; + + // ----------------------------------------------------------------------- + // State + // ----------------------------------------------------------------------- + + /** The Road Runner MecanumDrive used to command wheel powers. */ MecanumDrive drive; + + /** The pose the robot should return to if it is pushed away. + * {@code null} when the lock is inactive. */ private Pose2d referencePoint; + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an AntiDefense instance. + * + * @param telemetry currently unused; reserved for future debug output + * @param drive the MecanumDrive used to issue correction velocities + */ public AntiDefense(Telemetry telemetry, MecanumDrive drive) { - this.drive = drive; + this.drive = drive; // store the drive reference for update() to use } + + // ----------------------------------------------------------------------- + // Lock control + // ----------------------------------------------------------------------- + + /** + * Activates the pose lock at the given position. + * From this point on, {@link #update(Pose2d)} will try to return the robot + * to {@code currentBotPos} whenever it is pushed away. + * + * @param currentBotPos the field-relative pose to hold (usually the robot's + * current estimated pose at the moment of locking) + */ public void lock(Pose2d currentBotPos) { - referencePoint = currentBotPos; + referencePoint = currentBotPos; // save this pose as the correction target } + + /** + * Deactivates the pose lock. + * After calling this, {@link #update(Pose2d)} is a no-op until + * {@link #lock(Pose2d)} is called again. + */ public void unlock() { - referencePoint = null; + referencePoint = null; // null reference means "no lock active" } + + // ----------------------------------------------------------------------- + // Helper + // ----------------------------------------------------------------------- + + /** + * Wraps an angle (radians) into the range (-π, π]. + * + *

Road Runner's pose representation already normalises headings, but + * this helper is needed after {@code Pose2d.minusExp()} which may return + * a heading outside the standard range. + * + * @param angle heading error in radians + * @return equivalent angle in (-π, π] + */ private double clampAngle(double angle) { - angle = angle % (2 * Math.PI); - if (angle > Math.PI) angle -= 2 * Math.PI; - if (angle < -Math.PI) angle += 2 * Math.PI; + angle = angle % (2 * Math.PI); // reduce to (-2π, 2π) + if (angle > Math.PI) angle -= 2 * Math.PI; // fold the [π, 2π) half + if (angle < -Math.PI) angle += 2 * Math.PI; // fold the (-2π, -π] half return angle; } + + // ----------------------------------------------------------------------- + // Update loop + // ----------------------------------------------------------------------- + + /** + * Must be called every loop iteration while the lock is active. + * Computes the pose error between the reference point and the current + * robot position, and if the error exceeds the tolerances commands + * proportional correction drive powers. + * + *

If the lock is not active ({@link #referencePoint} is {@code null}) + * this method returns immediately without issuing any drive command. + * + * @param currentPos the robot's current estimated field-relative pose, + * typically from the Road Runner localizer + */ public void update(Pose2d currentPos) { + // Do nothing when the lock is not engaged. if(referencePoint == null) return; + + // Compute the spatial error: reference – current. + // minusExp() performs a pose-relative subtraction that correctly + // accounts for heading, giving the error in the robot's local frame. Pose2d error = referencePoint.minusExp(currentPos); - //double + + // Extract the translational (XY) and rotational (heading) components. Vector2d xyError = error.position; - double theta = clampAngle(error.heading.toDouble()); + double theta = clampAngle(error.heading.toDouble()); // wrap heading error to (-π, π] + // If the robot is already within both tolerance bands, no correction needed. if(xyError.norm() < xyTolerance && Math.abs(theta) < aTolerance) return; + + // Command a proportional velocity in the robot's local frame: + // - XY: position error scaled by kP gives a velocity vector pointing toward the reference. + // - Yaw: heading error scaled by kH gives a rotation rate to restore the heading. drive.setDrivePowers(new PoseVelocity2d(error.position.times(kP), theta * kH)); } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Indexer.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Indexer.java index 694ea4db566f..55de45988d98 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Indexer.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Indexer.java @@ -10,231 +10,684 @@ import org.firstinspires.ftc.teamcode.subsystems.indexerUtil.ColorSensorSystem; import org.firstinspires.ftc.teamcode.subsystems.indexerUtil.SlotState; +/** + * Indexer manages the three-slot rotating drum that holds and classifies game + * artifacts (game elements) before they are fired by the shooter. + * + *

Mechanical Overview

+ *

The drum is a circular magazine with three equally-spaced slots (0, 1, 2) + * separated by 120° of rotation. A continuous-rotation servo + * ({@link CRServoPositionControl}) drives the drum; an analog encoder tracks its + * absolute angle. A single colour sensor ({@link ColorSensorSystem}) is mounted + * at a fixed position; as the drum rotates, each slot passes in front of the sensor. + * + *

Colour Classification

+ *

Each slot is assigned one of four colours: + * {@link ArtifactColor#GREEN}, {@link ArtifactColor#PURPLE}, + * {@link ArtifactColor#EMPTY} (no artifact), or + * {@link ArtifactColor#UNKNOWN} (present but hue is ambiguous). + * Classification is probabilistic: multiple sensor reads are accumulated in a + * {@link org.firstinspires.ftc.teamcode.subsystems.indexerUtil.SlotObservation} + * per slot; the winner must exceed a configurable threshold. + * + *

Modes

+ * + * + *

Auto-Advancement Policies

+ * + * + *

Annotated with {@link Config} so all dashboard-tunable fields are editable + * live in the FTC Dashboard. + */ @Config public class Indexer { - public enum ArtifactColor { GREEN, PURPLE, EMPTY, UNKNOWN } + // ----------------------------------------------------------------------- + // Enums + // ----------------------------------------------------------------------- + + /** + * Represents the colour (or absence) of an artifact stored in a slot. + * Used both as the output of the colour classifier and as the stored + * state in each {@link SlotState}. + */ + public enum ArtifactColor { + /** The artifact is green (one of two colours used in this game). */ + GREEN, + /** The artifact is purple (the other game colour). */ + PURPLE, + /** No artifact is present in this slot. */ + EMPTY, + /** An artifact is detected but its colour cannot be reliably determined + * (sensor quality gate failed or hue is outside both defined ranges). */ + UNKNOWN + } + /** + * Names for each of the three drum positions. + * Each state holds its integer {@link #index} (0, 1, 2) and provides + * {@link #next()} and {@link #last()} helpers for traversing the ring. + */ public enum IndexerState { - zero(0), one(1), two(2); + /** Slot at index 0 (drum home position). */ + zero(0), + /** Slot at index 1 (120° from home). */ + one(1), + /** Slot at index 2 (240° from home). */ + two(2); + + /** The integer index of this slot (0, 1, or 2). */ public final int index; + + /** + * Constructs an IndexerState with the given index. + * @param index the position index of this slot + */ IndexerState(int index) { this.index = index; } + + /** + * Returns the next slot in the drum (wrapping from 2 → 0). + * @return the IndexerState immediately clockwise of this one + */ public IndexerState next() { return values()[(index + 1) % values().length]; } + + /** + * Returns the previous slot in the drum (wrapping from 0 → 2). + * @return the IndexerState immediately counter-clockwise of this one + */ public IndexerState last() { return values()[(index + values().length - 1) % values().length]; } } - // Dashboard control + // ----------------------------------------------------------------------- + // Dashboard overrides + // ----------------------------------------------------------------------- + + /** When {@code true} (rising edge), advances the drum by one slot. + * Set from FTC Dashboard for manual testing without a gamepad. */ public static boolean dashAdvance = false; + + /** Target slot index commanded from the FTC Dashboard. + * -1 = disabled; 0/1/2 forces the drum to that specific slot. + * Used to manually position the drum during tuning. */ public static int dashTargetSlot = -1; // -1 = disabled; 0/1/2 = slot - //Things that should be toggled depending on use case (more robust version coming) - // public boolean ENABLE_AUTO_ADVANCE = true; + // ----------------------------------------------------------------------- + // Auto-advancement policy enum + // ----------------------------------------------------------------------- + /** + * Selects how (or whether) the indexer automatically advances to the next + * empty slot during intaking. + */ public enum AutoAdvancement { + /** Never auto-advance; caller controls drum position entirely. */ DISABLED, + /** Advance to the next empty slot every loop iteration while the drum + * is aligned and an empty slot exists. */ SEEK_EMPTY, + /** Same as SEEK_EMPTY but only triggers when the sensor detects an + * artifact (avoids unnecessary drum movement when the intake is idle). */ QUICK_SEEK } + // ----------------------------------------------------------------------- + // Mode flags (configurable per use case) + // ----------------------------------------------------------------------- + + /** Current auto-advancement policy for this session. + * Should be set in the Hardware or OpMode constructor before use. */ public AutoAdvancement AUTO_ADVANCEMENT_MODE = AutoAdvancement.QUICK_SEEK; + /** When {@code true}, enables the full-drum "unknown scan" logic that + * rotates to unclassified slots to attempt a colour re-read when the drum + * is full but some slots remain UNKNOWN. Disable in autonomous to + * save time when the slot colours are already pre-known. */ public boolean ENABLE_FULL_UNKNOWN_SCAN = true; + + /** When {@code true}, the sensor classification pipeline runs every loop + * iteration to update slot colours. Setting to {@code false} disables + * all colour sensing (useful in autonomous where colours are pre-assigned). */ public boolean SCAN_COLORS = true; - // Auto-advance detection mode + // ----------------------------------------------------------------------- + // Auto-advance sensitivity (HARD vs SOFT) + // ----------------------------------------------------------------------- + + /** + * Selects whether auto-advancement requires many sensor confirmations (HARD) + * or few (SOFT) before accepting that an artifact has entered the slot. + * SOFT is faster but may false-trigger; HARD is slower but more robust. + */ public enum AutoDetectMode { HARD, SOFT } + + /** Currently active auto-detect mode (dashboard tunable). */ public static AutoDetectMode AUTO_DETECT_MODE = AutoDetectMode.SOFT; + + /** Number of consecutive sensor non-empty reads required in HARD mode + * before an artifact is considered fully seated in the current slot. */ public static int HARD_NONEMPTY_HITS_TO_ADVANCE = 8; + + /** Number of consecutive sensor non-empty reads required in SOFT mode + * before an artifact is considered fully seated in the current slot. */ public static int SOFT_NONEMPTY_HITS_TO_ADVANCE = 3; + // ----------------------------------------------------------------------- + // Drum angle offsets (degrees) + // ----------------------------------------------------------------------- - // Offsets + /** Base angle offset added to every slot's computed center angle. + * Calibrate so that {@code slot 0} aligns with the colour sensor when + * the encoder reads 0°. */ public static double offsetAngle = 92.0; + + /** Additional angle offset applied when the indexer is in autonomous-outtake + * mode ({@link #autoOuttaking} == true). Accounts for the difference between + * the intake sensor position and the shooter feed position during auto. */ public static double autoOuttakeOffsetAngle = 0.0; + + /** Additional angle offset applied when the indexer is in teleop-outtake + * mode ({@link #intaking} == false and {@link #autoOuttaking} == false). + * Rotates the drum ~186° so the slot that was facing the intake now faces + * the shooter barrel. */ public static double outtakeOffsetAngle = 186.0; - // Slot spacing for color sensing + // ----------------------------------------------------------------------- + // Slot geometry constants + // ----------------------------------------------------------------------- + + /** Angular distance (degrees) between consecutive slot centers. + * Three slots evenly spaced → 120° apart. */ private static final double SLOT_SPACING_DEG = 120.0; + + /** Maximum angular error (degrees) between the drum's measured position and + * a slot's center before observations are attributed to that slot. + * A tighter value prevents mis-attributing reads taken while the drum is + * still in transit. */ private static final double SLOT_ASSIGN_TOLERANCE = 15.0; - // Scan timing (kept for compatibility if later used) + // ----------------------------------------------------------------------- + // Scan timing + // ----------------------------------------------------------------------- + + /** Scaling factor: estimated servo travel time (ms) per degree of rotation. + * Computed as {@code scanDelayMs = deltaCW * msPerDegree} and clamped to + * [{@link #minWait}, {@link #maxWait}]. Kept for compatibility; may not + * currently be used by active code paths. */ private static final double msPerDegree = 0.6; + + /** Minimum scan delay (ms): always wait at least this long after issuing a + * moveTo command before reading the sensor to avoid transit-phase reads. */ private static final double minWait = 100; + + /** Maximum scan delay (ms): caps the computed delay even for large angular + * moves so the robot does not stall waiting for the sensor. */ private static final double maxWait = 300; - // Cooldown to avoid spamming moves while full+unknown + + /** Minimum milliseconds between consecutive unknown-slot scan attempts. + * Prevents the drum from spinning continuously when it is full of UNKNOWN + * slots and none of them can be resolved. */ public static long UNKNOWN_SCAN_COOLDOWN_MS = 250; - // Thresholds + // ----------------------------------------------------------------------- + // Colour classification thresholds (fractions of total observations) + // ----------------------------------------------------------------------- + + /** Fraction of readings that must be GREEN for a slot to be declared GREEN. + * Lower values make green detection more sensitive (easier to trigger). */ public static double GREEN_THRESHOLD = 0.2; + + /** Fraction of readings that must be PURPLE for a slot to be declared PURPLE. */ public static double PURPLE_THRESHOLD = 0.2; + + /** Fraction of readings that must be EMPTY for a slot to be declared EMPTY. + * High threshold (0.9) prevents spurious "empty" declarations when an artifact + * is briefly wobbling in the slot. */ public static double EMPTY_THRESHOLD = 0.9; + + /** Fraction of readings that must be UNKNOWN for a slot to be declared UNKNOWN. */ public static double UNKNOWN_THRESHOLD = 0.6; - // Minimum hits before allowing a color change + // ----------------------------------------------------------------------- + // Observation windowing + // ----------------------------------------------------------------------- + + /** Minimum number of sensor observations accumulated before a colour decision + * is made for a slot. Prevents snap decisions from single noisy reads. */ public static int MIN_HITS_FOR_DECISION = 4; - // Cap samples to keep responsiveness + + /** Maximum number of observations kept per slot (sliding-window cap). + * Older samples are proportionally scaled down by + * {@link org.firstinspires.ftc.teamcode.subsystems.indexerUtil.SlotObservation#trimToMax} + * when this limit is reached. */ public static int MAX_HITS_TO_KEEP = 20; + /** Angular tolerance (degrees) used by {@link #isWithinTargetDegrees} to check + * whether the drum has reached its commanded position. Used by auto-advance + * logic to confirm the drum is settled before calling {@link #findEmptySlot}. */ public static double ADVANCE_ANGLE_TOLERANCE = 5.0; - // Telemetry object + // ----------------------------------------------------------------------- + // Telemetry + // ----------------------------------------------------------------------- + + /** Optional telemetry output for debug. {@code null} means no telemetry output. */ private Telemetry telemetry = null; + + /** + * Attaches a telemetry object for debug output in the classification pipeline. + * @param t the FTC telemetry instance to write to; or {@code null} to disable + */ public void setTelemetry(Telemetry t) { telemetry = t; } - // Objects + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** Wrapped colour sensor that classifies artifacts as GREEN, PURPLE, EMPTY, or UNKNOWN. */ private final ColorSensorSystem colorSensor; + + /** Closed-loop position controller for the drum's continuous-rotation servo. */ private final CRServoPositionControl servoControl; + // ----------------------------------------------------------------------- // Internal state + // ----------------------------------------------------------------------- + + /** The currently targeted drum slot (the slot that is (or will be) aligned + * with the sensor or shooter). */ private IndexerState state = IndexerState.zero; + + /** {@code true} when the drum is oriented for intaking (sensor at intake opening); + * {@code false} when oriented for outtaking (shooter barrel position). */ private boolean intaking = true; + + /** {@code true} when an autonomous outtake sequence is active. Causes the + * drum to use {@link #autoOuttakeOffsetAngle} instead of the teleop offset. */ private boolean autoOuttaking = false; - private boolean loaded = false; // "anything present" (sensor OR stored memory) - private boolean noEmpty = false; // FULL: true when there are NO stored EMPTY slots + /** {@code true} when at least one artifact is present in the drum (either + * detected by the sensor right now, or stored as GREEN/PURPLE in a slot's memory). + * Used to switch between loaded and unloaded servo gains. */ + private boolean loaded = false; + + /** {@code true} when all three slot memories are non-EMPTY (GREEN, PURPLE, or UNKNOWN). + * Acts as the "drum is full" flag used to stop auto-advance from looking for an empty slot. */ + private boolean noEmpty = false; + + /** + * Returns whether all three slots in memory are occupied (no EMPTY slots stored). + * @return {@code true} if the drum is full + */ public boolean isFull() { return noEmpty; } + // ----------------------------------------------------------------------- // Per-slot state + // ----------------------------------------------------------------------- + + /** Array of three {@link SlotState} objects, one per drum slot (index 0, 1, 2). + * Each holds the stored colour, hit counts, and fill-cycle flags for that slot. */ private final SlotState[] slots = { - new SlotState(), - new SlotState(), - new SlotState() + new SlotState(), // slot 0 (index zero) + new SlotState(), // slot 1 (index one) + new SlotState() // slot 2 (index two) }; - // Scan scheduling + // ----------------------------------------------------------------------- + // Timers + // ----------------------------------------------------------------------- + + /** Timer used to compute the estimated scan delay after issuing a moveTo command. + * Compared against {@link #scanDelayMs} to know when the drum is likely on-target. */ private final ElapsedTime scanTimer = new ElapsedTime(); + + /** Estimated time (ms) for the drum to travel to the most recently commanded slot. */ private double scanDelayMs; - // Full+unknown scan cooldown + /** Cooldown timer for the unknown-slot scan logic. Prevents the drum from + * spinning continuously in a failed scan loop. */ private final ElapsedTime unknownScanTimer = new ElapsedTime(); - // Dashboard edge detection + // ----------------------------------------------------------------------- + // Dashboard edge-detection state + // ----------------------------------------------------------------------- + + /** Previous value of {@link #dashAdvance} used to detect rising edges + * (false→true transitions) rather than reacting every loop while it is held true. */ private boolean lastDashAdvance = false; + + /** Previous value of {@link #dashTargetSlot} used to detect changes so + * moveTo is only called once per slot selection, not every loop. */ private int lastDashTargetSlot = -1; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an Indexer instance. Retrieves the servo and encoder from the + * hardware map, constructs the control and sensor subsystems, and resets timers. + * + * @param hardwareMap the robot's hardware map used to resolve: + *

+ */ public Indexer(HardwareMap hardwareMap) { - CRServo servo = hardwareMap.get(CRServo.class, "index"); - AnalogInput analog = hardwareMap.get(AnalogInput.class, "indexAnalog"); + CRServo servo = hardwareMap.get(CRServo.class, "index"); // fetch the drum CR servo + AnalogInput analog = hardwareMap.get(AnalogInput.class, "indexAnalog"); // fetch the drum angle encoder + // Build the closed-loop servo controller using the servo and its encoder. servoControl = new CRServoPositionControl(servo, analog); + + // Build the colour sensor wrapper (fetches the sensor internally by name). colorSensor = new ColorSensorSystem(hardwareMap); - unknownScanTimer.reset(); + unknownScanTimer.reset(); // start the cooldown timer so the first scan isn't delayed } - // getters + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + + /** Returns the currently targeted drum slot. */ public IndexerState getState() { return state; } + + /** Returns {@code true} if the drum is in intaking mode. */ public boolean isIntaking() { return intaking; } + + /** + * Returns the stored colour for the specified slot. + * @param s the slot to query + * @return the {@link ArtifactColor} stored in that slot's memory + */ public ArtifactColor getColorAt(IndexerState s) { return slot(s).color; } + + /** Returns {@code true} if any artifact is detected in or remembered by the drum. */ public boolean isLoaded() { return loaded; } - /** Initialize all slots to the given color (default EMPTY) and reset observations/flags. */ + // ----------------------------------------------------------------------- + // Slot initialisation + // ----------------------------------------------------------------------- + + /** + * Resets all slot memories to {@link ArtifactColor#EMPTY} and clears all + * observations and fill-cycle flags. Call before each match or test to + * start from a clean state. + */ public void initializeColors() { initializeColors(ArtifactColor.EMPTY); } + /** + * Resets all slot memories to the given colour. Used at the start of an + * autonomous routine where all three slots are pre-loaded with a known colour. + * + * @param initialColor the colour to assign to every slot + */ public void initializeColors(ArtifactColor initialColor) { for (SlotState slot : slots) { - slot.color = initialColor; - slot.obs.reset(); - slot.wasEmpty = true; - slot.fillingHits = 0; - slot.fillCycleActive = false; + slot.color = initialColor; // assign the same color to every slot + slot.obs.reset(); // clear any previous sensor observations + slot.wasEmpty = true; // reset the transition-detection latch + slot.fillingHits = 0; // clear the fill-confirmation counter + slot.fillCycleActive = false; // deactivate any in-progress fill cycle } - recomputeNoEmpty(); + recomputeNoEmpty(); // update the full/not-full flag based on new colours } + /** + * Sets each slot to a specific colour. Used when the pre-game loading order + * is known (e.g., slot 0 = GREEN, slot 1 = PURPLE, slot 2 = PURPLE). + * + * @param one colour for slot 0 + * @param two colour for slot 1 + * @param three colour for slot 2 + */ public void initializeColors(ArtifactColor one, ArtifactColor two, ArtifactColor three) { - ArtifactColor[] colors = { one, two, three }; + ArtifactColor[] colors = { one, two, three }; // pack into array for indexed access for (int i = 0; i < slots.length; i++) { SlotState slot = slots[i]; - slot.color = colors[i]; - slot.obs.reset(); - slot.wasEmpty = true; - slot.fillingHits = 0; - slot.fillCycleActive = false; + slot.color = colors[i]; // assign the per-slot colour + slot.obs.reset(); // clear observation history + slot.wasEmpty = true; // reset transition latch + slot.fillingHits = 0; // clear fill counter + slot.fillCycleActive = false; // deactivate fill cycle } - recomputeNoEmpty(); + recomputeNoEmpty(); // update the full flag } + // ----------------------------------------------------------------------- + // Angle accessor + // ----------------------------------------------------------------------- + + /** + * Returns the drum's current measured angle, wrapped into [0, 360°). + * Reads the continuous angle from the servo controller and applies a + * positive-modulo wrap to ensure the value is always in range. + * + * @return current drum angle in degrees [0, 360) + */ public double getMeasuredAngle() { - return mod(servoControl.getCurrentAngle(), 360.0); + return mod(servoControl.getCurrentAngle(), 360.0); // wrap continuous angle to [0, 360) } - // api + // ----------------------------------------------------------------------- + // Mode setters + // ----------------------------------------------------------------------- + + /** + * Switches between intaking and outtaking orientation. + * If the mode actually changes, re-issues the current slot's target angle + * using the new offset so the servo adjusts immediately. + * + * @param isIntaking {@code true} to orient the drum for intaking; + * {@code false} for outtaking + */ public void setIntaking(boolean isIntaking) { if (this.intaking != isIntaking) { this.intaking = isIntaking; - moveTo(state, true); + moveTo(state, true); // force-reissue so the offset change takes effect immediately } } + /** + * Switches intake mode and simultaneously commands the drum to a specific slot. + * Used at the start of a match or after a shooting sequence to set both the + * mode and the initial slot position in one call. + * + * @param isIntaking {@code true} for intaking, {@code false} for outtaking + * @param moveToState the slot to move to after setting the mode + */ public void setIntaking(boolean isIntaking, IndexerState moveToState) { - this.intaking = isIntaking; - moveTo(moveToState, true); // forceRecommend true + this.intaking = isIntaking; // update mode flag + moveTo(moveToState, true); // forceRecommend = true ensures servo is re-commanded } + /** + * Enables or disables the autonomous-outtake angle offset. + * When {@code true}, slot angles use {@link #autoOuttakeOffsetAngle} instead + * of {@link #outtakeOffsetAngle}, which may differ mechanically from the + * teleop shooting position. Re-issues the current slot target after switching. + * + * @param isAutoOuttaking {@code true} to activate autonomous-outtake offset + */ public void setAutoOuttaking(boolean isAutoOuttaking) { this.autoOuttaking = isAutoOuttaking; - moveTo(state); + moveTo(state); // recompute and reissue target with the new offset } + // ----------------------------------------------------------------------- + // Colour-based movement + // ----------------------------------------------------------------------- + + /** + * Commands the drum to rotate to the best slot for the desired colour. + * "Best" is determined by {@link #scoreSlotForTarget}: a slot with an exact + * match scores 100; UNKNOWN scores 80; the other non-empty colour scores 60. + * EMPTY slots score 0 (skip them; an artifact is needed for outtaking). + * + * @param desired the target colour to seek + * @return {@code true} if a suitable slot was found and the move was issued; + * {@code false} if no non-empty slot exists for the desired colour + */ public boolean moveToColor(ArtifactColor desired) { - IndexerState target = findBestSlotForColor(desired); - if (target == null) return false; - moveTo(target); + IndexerState target = findBestSlotForColor(desired); // find the highest-scoring slot + if (target == null) return false; // no usable slot found; abort + moveTo(target); // command the drum to rotate return true; } + // ----------------------------------------------------------------------- + // Core moveTo methods + // ----------------------------------------------------------------------- + + /** + * Commands the drum to rotate to the given slot. + * If the target is already the current state, this is a no-op (no duplicate move). + * + * @param newState the slot to rotate to + */ public void moveTo(IndexerState newState) { - moveTo(newState, false); + moveTo(newState, false); // use default: do not force-reissue if already at target } - // Move to a slot and forceRecommend will reissue the target even if it's the current slot. + /** + * Commands the drum to rotate to the given slot, with optional force-reissue. + * + *

When {@code forceRecommend} is {@code true}, the target angle is always + * sent to the servo controller even if {@code newState == state}. This is + * needed when the angle offset has changed (e.g., switching intake/outtake mode) + * and the servo needs to move to a new physical position even if the logical + * slot index hasn't changed. + * + * @param newState the slot to rotate to + * @param forceRecommend if {@code true}, re-sends the angle even if already at target + */ public void moveTo(IndexerState newState, boolean forceRecommend) { - if (!forceRecommend && newState == state) return; + if (!forceRecommend && newState == state) return; // already there and no force; nothing to do - double targetAngle = getSlotCenterAngle(newState); + double targetAngle = getSlotCenterAngle(newState); // compute the physical angle for this slot + // Estimate the clockwise travel distance to compute a scan delay. double currentWrapped = getMeasuredAngle(); double deltaCW = targetAngle - currentWrapped; - if (deltaCW < 0) deltaCW += 360.0; + if (deltaCW < 0) deltaCW += 360.0; // wrap negative delta to positive clockwise arc + // Estimate travel time and clamp to safe bounds so sensors are not read mid-transit. scanDelayMs = clamp(deltaCW * msPerDegree, minWait, maxWait); - scanTimer.reset(); + scanTimer.reset(); // start the travel-time countdown - servoControl.clearOpenLoop(); - servoControl.moveToAngle(targetAngle); - state = newState; + servoControl.clearOpenLoop(); // ensure closed-loop mode is active (not blasting) + servoControl.moveToAngle(targetAngle); // issue the position command to the servo controller + state = newState; // update logical state immediately } - //sortedquickspin api + // ----------------------------------------------------------------------- + // Quickspin API + // ----------------------------------------------------------------------- + + /** + * Prepares the drum for a "quickspin" timed outtake by rotating to the + * correct starting slot so that artifacts exit in the desired colour order. + * + *

Prerequisites: + *

+ * + * @param desiredOrder the 3-element firing order to achieve; + * e.g., {@code {GREEN, PURPLE, PURPLE}} means GREEN fires first + * @return {@code true} if the drum was successfully rotated to the starting slot; + * {@code false} if validation failed (wrong colour mix or invalid input) + */ public boolean prepareQuickspin(ArtifactColor[] desiredOrder) { - // Validate input - if (desiredOrder == null || desiredOrder.length != 3) return false; - if (!isTwoPurpleOneGreen(desiredOrder)) return false; + if (desiredOrder == null || desiredOrder.length != 3) return false; // input sanity check + if (!isTwoPurpleOneGreen(desiredOrder)) return false; // must be exactly 2P + 1G - // Validate stored slots - if (!storedSlotsValidForQuickspin()) return false; + if (!storedSlotsValidForQuickspin()) return false; // drum must contain 2P + 1G - // Quickspin must start in intaking mode - setIntaking(true); + setIntaking(true); // quickspin must start in intaking mode for correct angle offsets - // Find correct starting slot + // Search all three starting positions for one that produces the desired order. for (IndexerState s : IndexerState.values()) { int x = s.index; if (matchesQuickspin(x, desiredOrder)) { - moveTo(s); + moveTo(s); // found the right starting position; rotate there return true; } } - // Should never happen if validation passed + // Should never reach here if validation passed (all permutations were checked above). return false; } + // ----------------------------------------------------------------------- + // Open-loop power control (used during timed outtake blast) + // ----------------------------------------------------------------------- + + /** + * Commands the servo to run at a fixed open-loop power. + * Overrides the closed-loop position controller until + * {@link #stopIndexerPower()} is called. Used during the "full blast" + * outtake where the drum spins continuously at a set power for a timed duration. + * + * @param power servo power in [-1.0, 1.0]; positive = forward rotation + */ /** Open-loop blast: full power until caller stops it (or sets another power). */ public void setIndexerPower(double power) { servoControl.setOpenLoopPower(power); } - public void stopIndexerPower() { servoControl.setOpenLoopPower(0); servoControl.clearOpenLoop(); } + /** + * Stops the open-loop blast and returns to closed-loop mode. + * Sets power to 0 first, then clears the override flag so the + * position controller resumes on the next {@link #update()} call. + */ + public void stopIndexerPower() { + servoControl.setOpenLoopPower(0); // zero the motor first + servoControl.clearOpenLoop(); // re-enable closed-loop position control + } + + // ----------------------------------------------------------------------- + // Main update loop + // ----------------------------------------------------------------------- + + /** + * Must be called every robot loop iteration to keep the indexer running. + * + *

Processing order: + *

    + *
  1. Dashboard overrides – handle manual slot advance and target-slot commands.
  2. + *
  3. Loaded state + servo – determine if any artifact is present (sensor or memory), + * apply loaded/unloaded gains, run one iteration of the servo PD controller.
  4. + *
  5. Colour classification – if intaking, scanning enabled, and not all classified, + * record a sensor sample for the closest slot and update its colour memory.
  6. + *
  7. Full flag recompute – update {@link #noEmpty} based on current slot memories.
  8. + *
  9. Auto-advance – if an empty slot exists and the policy requests it, move there.
  10. + *
  11. Unknown scan – if the drum is full but some slots are UNKNOWN, rotate to them.
  12. + *
+ */ // update loop: // 1) Handle dashboard overrides // 2) Update loaded state and servo control @@ -243,133 +696,226 @@ public boolean prepareQuickspin(ArtifactColor[] desiredOrder) { // 5) If intaking and any EMPTY exists, always seek an EMPTY slot (skip UNKNOWN too) // 6) If full and still have UNKNOWN slots, optionally move to unknown slots for rescan public void update() { - handleDashboardCommands(); - refreshLoadedAndServo(); + handleDashboardCommands(); // step 1: process any FTC Dashboard manual overrides + refreshLoadedAndServo(); // step 2: update sensor presence, loaded state, servo PD + + // Step 3: run colour classification only while actively intaking and scanning is on, + // and only while at least one slot is still unclassified (avoid unnecessary work). if (intaking && SCAN_COLORS && !allClassified()) { - updateSlotClassification(debugClosestSlot()); + updateSlotClassification(debugClosestSlot()); // classify the slot currently under the sensor } - // Update full flag every loop based on stored memory + // Step 4: update the FULL flag based on latest slot colour memories. recomputeNoEmpty(); + // Step 5a: SEEK_EMPTY auto-advance – move to an empty slot every iteration if settled. if (AUTO_ADVANCEMENT_MODE == AutoAdvancement.SEEK_EMPTY && intaking && !noEmpty && isWithinTargetDegrees(ADVANCE_ANGLE_TOLERANCE)) { - moveTo(findEmptySlot()); + moveTo(findEmptySlot()); // advance to the nearest empty slot } + + // Step 5b: QUICK_SEEK auto-advance – only advance when the sensor currently sees an artifact. if(AUTO_ADVANCEMENT_MODE == AutoAdvancement.QUICK_SEEK) { + // Trigger only if: artifact currently detected AND drum has an empty slot AND drum is settled. if(colorSensor.hasArtifact() && !noEmpty && isWithinTargetDegrees(ADVANCE_ANGLE_TOLERANCE)) { - moveTo(findEmptySlot()); + moveTo(findEmptySlot()); // move to the next available empty slot } } - // If full and still have UNKNOWN slots, go look at them (intaking only) + + // Step 6: if drum is full but some slots are still UNKNOWN, rotate to them for a rescan. + // A cooldown prevents spinning the drum in a tight loop if the colour can't be determined. if (ENABLE_FULL_UNKNOWN_SCAN && intaking && noEmpty && unknownScanTimer.milliseconds() >= UNKNOWN_SCAN_COOLDOWN_MS) { IndexerState target = findNextSlotWithStoredColor(state.last(), ArtifactColor.UNKNOWN); if (target != null) { - moveTo(target); - unknownScanTimer.reset(); + moveTo(target); // rotate to the unclassified slot for re-read + unknownScanTimer.reset(); // reset cooldown so we don't spam this move } } } + // ----------------------------------------------------------------------- + // Private: dashboard override handling + // ----------------------------------------------------------------------- + + /** + * Handles the FTC Dashboard advance and target-slot commands. + * Called once per {@link #update()} iteration. + * Uses edge detection to execute commands exactly once per dashboard change. + */ private void handleDashboardCommands() { + // Rising-edge detection: only advance once when dashAdvance goes false→true. if (dashAdvance && !lastDashAdvance) { - moveTo(state.next()); + moveTo(state.next()); // advance to the next slot (wraps from 2 → 0) } - lastDashAdvance = dashAdvance; + lastDashAdvance = dashAdvance; // store current value for next-iteration comparison + // Direct target-slot: only re-issue moveTo when the slot selection changes. if (dashTargetSlot != lastDashTargetSlot) { if (dashTargetSlot >= 0 && dashTargetSlot <= 2) { - moveTo(IndexerState.values()[dashTargetSlot], true); + moveTo(IndexerState.values()[dashTargetSlot], true); // force-move to the selected slot } - lastDashTargetSlot = dashTargetSlot; + lastDashTargetSlot = dashTargetSlot; // store for next-iteration comparison } } + // ----------------------------------------------------------------------- + // Private: loaded state and servo update + // ----------------------------------------------------------------------- + + /** + * Determines the "loaded" flag (is any artifact present?) and runs one + * iteration of the servo position controller. + * + *

The loaded flag is {@code true} when either: + *

+ * The flag is passed to {@link CRServoPositionControl#setLoaded(boolean)} to + * switch between the heavier "loaded" PD gains and lighter "unloaded" gains. + */ private void refreshLoadedAndServo() { - // Determine loaded state BEFORE control update + // Start with the live sensor reading. boolean hasAnyArtifact = colorSensor.hasArtifact(); + + // Also check stored slot memories: if any slot remembers a real artifact, we're loaded. for (SlotState slot : slots) { if (slot.color == ArtifactColor.GREEN || slot.color == ArtifactColor.PURPLE) { - hasAnyArtifact = true; + hasAnyArtifact = true; // confirmed artifact in memory; no need to check further break; } } - loaded = hasAnyArtifact; - servoControl.setLoaded(loaded); - servoControl.update(); + + loaded = hasAnyArtifact; // update cached loaded flag + servoControl.setLoaded(loaded); // switch servo gains based on load state + servoControl.update(); // run one PD control iteration } - // slot geometry + // ----------------------------------------------------------------------- + // Private: slot geometry + // ----------------------------------------------------------------------- + + /** + * Computes the physical drum angle (degrees) that aligns the given slot + * with the active sensing/shooting position, accounting for the current mode. + * + * @param s the slot whose center angle to compute + * @return drum angle in [0, 360°) for the given slot in the current mode + */ private double getSlotCenterAngle(IndexerState s) { - double angle = s.index * SLOT_SPACING_DEG; - angle += offsetAngle; + double angle = s.index * SLOT_SPACING_DEG; // base angle: 0°, 120°, or 240° + angle += offsetAngle; // apply calibration zero-offset if (autoOuttaking) { - angle += autoOuttakeOffsetAngle; // NOT 180 unless confirmed mechanically + angle += autoOuttakeOffsetAngle; // autonomous outtake uses a separate offset } else if (!intaking) { - angle += outtakeOffsetAngle; // NOT 180 unless confirmed mechanically + angle += outtakeOffsetAngle; // teleop outtake: rotate ~186° to face shooter } + // In intaking mode no additional offset is applied (sensor is at 0 + offsetAngle). - return mod(angle, 360.0); + return mod(angle, 360.0); // wrap to [0, 360) so angles are unambiguous } + // ----------------------------------------------------------------------- + // Colour assignment (manual override) + // ----------------------------------------------------------------------- + + /** + * Manually assigns a colour to a specific slot and resets its observation + * history. Used by autonomous routines where the pre-loaded colours are known, + * so the sensor does not need to re-classify. + * + * @param slotState the slot to assign + * @param color the colour to store in that slot's memory + */ public void assignSlotColor(IndexerState slotState, ArtifactColor color) { SlotState slot = slot(slotState); - // Assign color - slot.color = color; + slot.color = color; // set the stored colour directly, bypassing the sensor - // Reset observations so classifier doesn't fight this - slot.obs.reset(); + slot.obs.reset(); // clear sensor observations so the classifier doesn't override this assignment - // Update empty tracking + // Update the empty-tracking latch based on the new assigned colour. boolean isEmpty = (color == ArtifactColor.EMPTY || color == ArtifactColor.UNKNOWN); - slot.wasEmpty = isEmpty; - slot.fillingHits = 0; - slot.fillCycleActive = false; + slot.wasEmpty = isEmpty; // true for empty/unknown; false for green/purple + slot.fillingHits = 0; // clear fill counter + slot.fillCycleActive = false; // deactivate fill cycle - recomputeNoEmpty(); + recomputeNoEmpty(); // update the full flag with the new assignment } + // ----------------------------------------------------------------------- + // Private: angle helpers + // ----------------------------------------------------------------------- + + /** + * Computes the absolute angular error (shortest path) between two angles. + * + * @param a first angle in degrees + * @param b second angle in degrees + * @return absolute angular difference in [0, 180°] + */ private double angleError(double a, double b) { + // Shift by 180°, take mod 360°, subtract 180° to fold into [-180, 180], then abs. return Math.abs(mod(a - b + 180.0, 360.0) - 180.0); } - // classification + auto-advance + // ----------------------------------------------------------------------- + // Private: classification and telemetry + // ----------------------------------------------------------------------- + + /** + * Reads the colour sensor and records one observation for each slot that is + * currently within {@link #SLOT_ASSIGN_TOLERANCE} of the sensor position. + * After accumulating {@link #MIN_HITS_FOR_DECISION} samples, resolves the + * probable colour and updates the slot's memory. + * + *

The "protect known" guard prevents a good GREEN or PURPLE classification + * from being overwritten by a transient UNKNOWN or EMPTY read (e.g., if the + * artifact wiggles slightly and the sensor briefly loses it). + * + * @param currentSlot the slot that was closest to the sensor on this cycle + * (used to gate telemetry output to only the active slot) + */ private void updateSlotClassification(IndexerState currentSlot) { - double currentAngle = getMeasuredAngle(); + double currentAngle = getMeasuredAngle(); // current drum angle in [0, 360) + // Check every slot: multiple slots could theoretically be within tolerance + // (shouldn't happen with 120° spacing and a 15° tolerance, but we check all for robustness). for (IndexerState s : IndexerState.values()) { double err = angleError(currentAngle, getSlotCenterAngle(s)); - if (err > SLOT_ASSIGN_TOLERANCE) continue; // not over sensor + if (err > SLOT_ASSIGN_TOLERANCE) continue; // slot is not under the sensor; skip SlotState slot = slot(s); + // Read the sensor: presence check determines whether to classify or mark EMPTY. boolean hasArtifact = colorSensor.hasArtifact(); ArtifactColor instantColor = hasArtifact - ? colorSensor.classifyColorOnly() - : ArtifactColor.EMPTY; + ? colorSensor.classifyColorOnly() // artifact present: determine its colour + : ArtifactColor.EMPTY; // nothing there; record EMPTY - // If transitioning from empty->nonempty (sensor), reset observation window for faster settle + // If this slot was previously empty and now sees something, reset observations + // so old EMPTY counts don't dilute the new non-empty classification. if (slot.wasEmpty && hasArtifact) { - slot.obs.reset(); + slot.obs.reset(); // wipe the old observation window for a clean re-read } - slot.obs.record(instantColor); - slot.obs.trimToMax(MAX_HITS_TO_KEEP); + slot.obs.record(instantColor); // add this sample to the observation window + slot.obs.trimToMax(MAX_HITS_TO_KEEP); // cap the window size to prevent stale data buildup - int total = slot.obs.totalHits(); + int total = slot.obs.totalHits(); // total samples in the current observation window if (total >= MIN_HITS_FOR_DECISION) { + // Enough data: try to resolve a colour from the accumulated observations. ArtifactColor candidate = slot.obs.resolveWithThreshold( GREEN_THRESHOLD, PURPLE_THRESHOLD, @@ -379,18 +925,21 @@ private void updateSlotClassification(IndexerState currentSlot) { ArtifactColor currentColor = slot.color; + // "Protect known" guard: if the slot is already classified as GREEN or PURPLE, + // don't let a transient UNKNOWN/EMPTY candidate overwrite it. boolean protectKnown = (candidate == ArtifactColor.UNKNOWN || candidate == ArtifactColor.EMPTY) && (currentColor == ArtifactColor.GREEN || currentColor == ArtifactColor.PURPLE); if (!protectKnown && candidate != currentColor) { - slot.color = candidate; + slot.color = candidate; // update the stored colour to the new classification } } - // Update memory ONLY + // Update the empty-transition latch based solely on the sensor (not the stored colour). slot.wasEmpty = !hasArtifact; + // Output detailed telemetry for the currently active slot only. if (telemetry != null && s == currentSlot) { int totalHitsTelemetry = slot.obs.totalHits(); telemetry.addData("Loaded", loaded); @@ -398,19 +947,29 @@ private void updateSlotClassification(IndexerState currentSlot) { telemetry.addData("Slot " + s + " hit %", String.format( "G: %.0f%%, P: %.0f%%, E: %.0f%%, U: %.0f%%", - totalHitsTelemetry > 0 ? slot.obs.greenHits * 100.0 / totalHitsTelemetry : 0, + totalHitsTelemetry > 0 ? slot.obs.greenHits * 100.0 / totalHitsTelemetry : 0, totalHitsTelemetry > 0 ? slot.obs.purpleHits * 100.0 / totalHitsTelemetry : 0, - totalHitsTelemetry > 0 ? slot.obs.emptyHits * 100.0 / totalHitsTelemetry : 0, - totalHitsTelemetry > 0 ? slot.obs.unknownHits * 100.0 / totalHitsTelemetry : 0 + totalHitsTelemetry > 0 ? slot.obs.emptyHits * 100.0 / totalHitsTelemetry : 0, + totalHitsTelemetry > 0 ? slot.obs.unknownHits* 100.0 / totalHitsTelemetry : 0 )); - colorSensor.addTelemetry(telemetry); + colorSensor.addTelemetry(telemetry); // add full sensor HSV breakdown } } } - // debug + // ----------------------------------------------------------------------- + // Debug / telemetry helpers + // ----------------------------------------------------------------------- + + /** + * Returns the slot whose physical center angle is closest to the drum's + * current measured angle. Used during classification to determine which + * slot is currently under the sensor. + * + * @return the nearest {@link IndexerState} to the current drum position + */ public IndexerState debugClosestSlot() { - double current = getMeasuredAngle(); + double current = getMeasuredAngle(); // current drum angle IndexerState best = null; double bestErr = Double.MAX_VALUE; @@ -418,26 +977,53 @@ public IndexerState debugClosestSlot() { for (IndexerState s : IndexerState.values()) { double err = angleError(current, getSlotCenterAngle(s)); if (err < bestErr) { - bestErr = err; + bestErr = err; // found a closer slot best = s; } } - return best; + return best; // the slot with the smallest angular error to current position } + /** + * Returns the angular error (degrees) between the drum's current position and + * the closest slot's center. Useful in telemetry to assess positional accuracy. + * + * @return absolute angular error in [0, 180°] + */ public double debugClosestSlotErrorDeg() { - IndexerState s = debugClosestSlot(); - return angleError(getMeasuredAngle(), getSlotCenterAngle(s)); + IndexerState s = debugClosestSlot(); // find the closest slot first + return angleError(getMeasuredAngle(), getSlotCenterAngle(s)); // compute error to that slot } + /** + * Returns the angular error (degrees) between the drum's current position and + * the center angle of the specified slot. + * + * @param s the slot to measure against + * @return absolute angular error in [0, 180°] + */ public double debugSlotErrorDeg(IndexerState s) { return angleError(getMeasuredAngle(), getSlotCenterAngle(s)); } + /** + * Returns {@code true} if the given slot is currently within + * {@link #SLOT_ASSIGN_TOLERANCE} of the sensor – i.e., the slot is "over" it. + * + * @param s the slot to test + * @return {@code true} if the slot is aligned with the colour sensor + */ public boolean debugSlotIsOverSensor(IndexerState s) { return debugSlotErrorDeg(s) <= SLOT_ASSIGN_TOLERANCE; } + /** + * Returns a human-readable string describing the drum's current alignment status. + * Used for telemetry dashboards during debugging. + * + * @return description such as {@code "Aligned with zero"} or + * {@code "Between slots (err=14.2 degrees)"} + */ public String debugAssignmentReason() { IndexerState s = debugClosestSlot(); double err = debugClosestSlotErrorDeg(); @@ -448,6 +1034,13 @@ public String debugAssignmentReason() { return "Aligned with " + s; } + /** + * Returns the angular error of the second-closest slot (degrees). + * Used to verify that the closest-slot algorithm is not confused by two slots + * being nearly equidistant (which would indicate the drum is exactly between slots). + * + * @return angular error of the second-nearest slot to the current position + */ public double debugSecondClosestSlotErrorDeg() { double current = getMeasuredAngle(); double best = Double.MAX_VALUE; @@ -456,127 +1049,232 @@ public double debugSecondClosestSlotErrorDeg() { for (IndexerState s : IndexerState.values()) { double err = angleError(current, getSlotCenterAngle(s)); if (err < best) { - second = best; + second = best; // demote the previous best to second best = err; } else if (err < second) { - second = err; + second = err; // new second-best } } return second; } + /** + * Returns the raw encoder voltage from the drum's analog encoder. + * @return encoder voltage in volts + */ public double getVoltage() { return servoControl.getVoltage(); } + + /** + * Returns the expected encoder voltage that corresponds to the current target angle. + * @return target voltage in volts + */ public double getTargetVoltage() { return servoControl.getTargetVoltage(); } - /* ========================= - UTIL - ========================= */ + // ----------------------------------------------------------------------- + // Private utility methods + // ----------------------------------------------------------------------- + /** + * Returns the number of consecutive non-empty sensor hits required before + * auto-advancing, based on the current {@link #AUTO_DETECT_MODE}. + * + * @return hit threshold for the current mode + */ private int requiredNonEmptyHitsToAdvance() { return (AUTO_DETECT_MODE == AutoDetectMode.HARD) - ? HARD_NONEMPTY_HITS_TO_ADVANCE - : SOFT_NONEMPTY_HITS_TO_ADVANCE; + ? HARD_NONEMPTY_HITS_TO_ADVANCE // conservative: 8 hits + : SOFT_NONEMPTY_HITS_TO_ADVANCE; // responsive: 3 hits } + /** + * Recomputes the {@link #noEmpty} flag by scanning all slot memories. + * {@code noEmpty} is {@code true} if and only if no slot stores EMPTY. + */ private void recomputeNoEmpty() { boolean anyEmpty = false; for (SlotState slot : slots) { if (slot.color == ArtifactColor.EMPTY) { - anyEmpty = true; + anyEmpty = true; // found an empty slot; short-circuit break; } } - noEmpty = !anyEmpty; + noEmpty = !anyEmpty; // full = no empty slots remain } + /** + * Returns {@code true} when every slot has been classified as either + * GREEN or PURPLE (i.e., no UNKNOWN or EMPTY slots remain). + * Used to skip colour scanning once all colours are known. + * + * @return {@code true} if all slots have a definitive colour + */ private boolean allClassified() { //checks if all are assigned a color (no unknown or empty) for (SlotState slot : slots) { if (slot.color == ArtifactColor.UNKNOWN || slot.color == ArtifactColor.EMPTY) { - return false; + return false; // at least one slot is unresolved } } - return true; + return true; // every slot has been assigned GREEN or PURPLE } + /** + * Searches the drum for the next slot with the given stored colour, starting + * from {@code start} and iterating forward (by {@code next()}) up to 3 times. + * + * @param start the starting position (search begins at start.next()) + * @param color the colour to search for + * @return the first matching {@link IndexerState}, or {@code null} if none found + */ private IndexerState findNextSlotWithStoredColor(IndexerState start, ArtifactColor color) { IndexerState s = start; for (int i = 0; i < 3; i++) { - s = s.next(); - if (slot(s).color == color) return s; + s = s.next(); // advance to the next slot in the ring + if (slot(s).color == color) return s; // found a matching slot } - return null; + return null; // no slot with the desired colour found after checking all three } + /** + * Finds the slot with the highest score for the desired colour using + * {@link #scoreSlotForTarget}. Returns the {@link IndexerState} with the + * best match, or {@code null} if no slot scores above 0. + * + * @param desired the colour to search for + * @return the best-matching slot, or {@code null} if all slots are EMPTY + */ public IndexerState findBestSlotForColor(ArtifactColor desired) { IndexerState best = null; int bestScore = -1; for (IndexerState s : IndexerState.values()) { - ArtifactColor slotColor = slot(s).color; // use live slot color - int score = scoreSlotForTarget(desired, slotColor); + ArtifactColor slotColor = slot(s).color; // read the stored colour for this slot + int score = scoreSlotForTarget(desired, slotColor); // score this slot if (score > bestScore) { - bestScore = score; + bestScore = score; // new best best = s; } } - // If all slots are EMPTY or useless, don't move + // If all slots scored 0 (all empty) there is nothing useful to fire. if (bestScore <= 0) return null; return best; } + /** + * Returns the first slot (searching forward from {@link #state}.last()) + * whose stored colour is {@link ArtifactColor#EMPTY}. + * + * @return the nearest empty slot, or {@code null} if none exist + */ public IndexerState findEmptySlot() { + // Start from state.last() so we search all three slots including the current one. return findNextSlotWithStoredColor(state.last(), ArtifactColor.EMPTY); } + /** + * Returns an empty slot, optionally excluding the current slot. + * + * @param includeCurrent if {@code true}, the current slot may be returned; + * if {@code false}, only the other two slots are checked + * @return the nearest qualifying empty slot, or {@code null} if none exist + */ public IndexerState findEmptySlot(boolean includeCurrent) { if(includeCurrent) - return findEmptySlot(); + return findEmptySlot(); // search from state.last() (includes current) else - return findNextSlotWithStoredColor(state.next(), ArtifactColor.EMPTY); + return findNextSlotWithStoredColor(state.next(), ArtifactColor.EMPTY); // skip current; search forward } + /** + * Returns a priority score indicating how well a slot matches the desired colour. + * Higher scores are more desirable for outtaking. + * + *

Scoring: + *

+ * + * @param desired the colour we want to fire + * @param slotColor the colour stored in the candidate slot + * @return integer score in {0, 60, 80, 100} + */ // gives preferences private int scoreSlotForTarget(ArtifactColor desired, ArtifactColor slotColor) { - if (slotColor == desired) return 100; + if (slotColor == desired) return 100; // perfect match; highest priority if (desired == ArtifactColor.GREEN) { - if (slotColor == ArtifactColor.UNKNOWN) return 80; - if (slotColor == ArtifactColor.PURPLE) return 60; - if (slotColor == ArtifactColor.EMPTY) return 0; + if (slotColor == ArtifactColor.UNKNOWN) return 80; // might be green; take a chance + if (slotColor == ArtifactColor.PURPLE) return 60; // wrong colour but non-empty; use as fallback + if (slotColor == ArtifactColor.EMPTY) return 0; // no artifact; skip } if (desired == ArtifactColor.PURPLE) { - if (slotColor == ArtifactColor.UNKNOWN) return 80; - if (slotColor == ArtifactColor.GREEN) return 60; - if (slotColor == ArtifactColor.EMPTY) return 0; + if (slotColor == ArtifactColor.UNKNOWN) return 80; // might be purple; take a chance + if (slotColor == ArtifactColor.GREEN) return 60; // wrong colour but non-empty; use as fallback + if (slotColor == ArtifactColor.EMPTY) return 0; // no artifact; skip } - return 0; + return 0; // default: this slot has no value for the desired colour } + /** + * Returns {@code true} if the drum is currently within {@code toleranceDeg} degrees + * of its commanded slot center. Used to confirm that the servo has settled at + * the target position before performing an operation that requires alignment. + * + * @param toleranceDeg maximum acceptable angular error in degrees + * @return {@code true} if the drum is settled at the current target slot + */ public boolean isWithinTargetDegrees(double toleranceDeg) { - double measured = getMeasuredAngle(); - double target = getSlotCenterAngle(state); - return angleError(measured, target) <= toleranceDeg; + double measured = getMeasuredAngle(); // read current drum angle + double target = getSlotCenterAngle(state); // compute the expected angle for the current slot + return angleError(measured, target) <= toleranceDeg; // true if error is within tolerance } + // ----------------------------------------------------------------------- + // Private math helpers + // ----------------------------------------------------------------------- + + /** Clamps {@code v} to the range [min, max]. */ private double clamp(double v, double min, double max) { return Math.max(min, Math.min(max, v)); } + /** + * Positive modulo: always returns a non-negative result in [0, m). + * Java's {@code %} can return negative values for negative operands. + */ private double mod(double v, double m) { double r = v % m; - return r < 0 ? r + m : r; + return r < 0 ? r + m : r; // shift negative remainder into [0, m) } + /** + * Convenience accessor: returns the {@link SlotState} for the given slot index. + * @param s the slot whose state to retrieve + * @return the SlotState object for slot {@code s} + */ private SlotState slot(IndexerState s) { return slots[s.index]; } + // ----------------------------------------------------------------------- + // Private quickspin helpers + // ----------------------------------------------------------------------- + + /** + * Returns {@code true} if the array contains exactly 2 PURPLE and 1 GREEN, + * which is the only valid firing mix for a quickspin outtake in this game. + * + * @param arr a 3-element colour array + * @return {@code true} if the array is valid (2P + 1G, no other colours) + */ //quickspin helper private boolean isTwoPurpleOneGreen(ArtifactColor[] arr) { int purple = 0; @@ -585,11 +1283,17 @@ private boolean isTwoPurpleOneGreen(ArtifactColor[] arr) { for (ArtifactColor c : arr) { if (c == ArtifactColor.PURPLE) purple++; else if (c == ArtifactColor.GREEN) green++; - else return false; + else return false; // any other colour (EMPTY, UNKNOWN) makes this invalid } - return purple == 2 && green == 1; + return purple == 2 && green == 1; // must be exactly 2 purple and 1 green } + /** + * Returns {@code true} if the drum's stored slot colours are valid for a + * quickspin (all three slots must contain exactly 2 PURPLE + 1 GREEN). + * + * @return {@code true} if the drum is loaded with the correct mix + */ private boolean storedSlotsValidForQuickspin() { int purple = 0; int green = 0; @@ -597,21 +1301,49 @@ private boolean storedSlotsValidForQuickspin() { for (SlotState slot : slots) { if (slot.color == ArtifactColor.PURPLE) purple++; else if (slot.color == ArtifactColor.GREEN) green++; - else return false; + else return false; // EMPTY or UNKNOWN slots disqualify quickspin } - return purple == 2 && green == 1; + return purple == 2 && green == 1; // valid only if exactly 2P + 1G are stored } + /** + * Checks whether starting a quickspin at position {@code x} would produce + * the desired firing order. + * + *

During a forward-spin blast the drum fires slots in the order: + * {@code (x+2)%3 → x → (x+1)%3}, so we verify that the stored colours + * at those positions match {@code desired[0]}, {@code desired[1]}, and + * {@code desired[2]} respectively. + * + * @param x the candidate starting slot index (0, 1, or 2) + * @param desired the 3-element desired firing order + * @return {@code true} if starting at slot {@code x} produces the desired order + */ //firing order:(x + 2) % 3, x, (x + 1) % 3 private boolean matchesQuickspin(int x, ArtifactColor[] desired) { - return slots[(x + 2) % 3].color == desired[0] - && slots[x].color == desired[1] - && slots[(x + 1) % 3].color == desired[2]; + return slots[(x + 2) % 3].color == desired[0] // first artifact to fire + && slots[x].color == desired[1] // second artifact to fire + && slots[(x + 1) % 3].color == desired[2]; // third artifact to fire } + // ----------------------------------------------------------------------- + // Public convenience check + // ----------------------------------------------------------------------- + + /** + * Returns {@code true} if the colour sensor detects an artifact and + * the drum is within 15° of the current target slot. + * + *

This combined check is used by the feedback-based intake action in + * {@link org.firstinspires.ftc.teamcode.auto.utils.BotActions} to determine + * when an artifact has been successfully collected: the indexer must be aligned + * (drum settled) and the sensor must confirm presence. + * + * @return {@code true} if an artifact is both detected and the drum is aligned + */ public boolean artifactPresentAndAligned() { - boolean hasArtifact = colorSensor.hasArtifact(); - boolean aligned = isWithinTargetDegrees(15.0); - return hasArtifact && aligned; + boolean hasArtifact = colorSensor.hasArtifact(); // sensor presence check + boolean aligned = isWithinTargetDegrees(15.0); // drum position check + return hasArtifact && aligned; // both conditions must be true simultaneously } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Intake.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Intake.java index 7ddde54e3c09..4ffe95febb50 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Intake.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Intake.java @@ -4,40 +4,139 @@ import com.arcrobotics.ftclib.hardware.motors.MotorEx; import com.qualcomm.robotcore.hardware.HardwareMap; +/** + * Intake manages the robot's ground intake roller motor. + * + *

The intake roller sweeps game elements (artifacts) from the field floor + * into the indexer drum. Positive "intaking" direction is configured so that + * setting power to {@link #INTAKING_POWER} (a negative value) spins the motor + * in the direction that pulls artifacts in. Reversing the sign ejects artifacts + * back onto the field, which is useful for clearing jams. + * + *

A {@link #SLOW_MULTIPLIER} is provided so that, when the indexer is + * actively aligning a freshly-collected artifact, the intake can be slowed + * down to prevent forcing a second artifact in before the first has seated. + * + *

Annotated with {@link Config} so {@code SLOW_MULTIPLIER} and + * {@code INTAKING_POWER} can be tuned live via FTC Dashboard. + */ @Config public class Intake { + + // ----------------------------------------------------------------------- + // Dashboard-tunable power constants + // ----------------------------------------------------------------------- + + /** Fraction (0–1) applied to {@link #INTAKING_POWER} when running at reduced + * speed. A value of 0.5 means 50 % of full intake power. Used to slow the + * roller while the indexer repositions so a second artifact doesn't overrun + * a freshly-collected one. */ public static double SLOW_MULTIPLIER = 0.5; + + /** Raw motor power applied during normal full-speed intaking. Negative because + * the motor's forward direction physically ejects artifacts; reversing it pulls + * them into the robot. */ public static double INTAKING_POWER = -1; + + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** FTCLib extended motor wrapper for the intake roller, mapped to the + * hardware config name {@code "intake"}. */ private MotorEx intakeMotor; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an Intake instance and looks up the motor in the hardware map. + * + * @param hardwareMap the robot's hardware map used to resolve the motor + * registered under the configuration name {@code "intake"} + */ public Intake(HardwareMap hardwareMap) { + // Retrieve the intake roller motor from the hardware map using its + // config name. MotorEx wraps DcMotorEx with additional helper methods. intakeMotor = new MotorEx(hardwareMap, "intake"); } + + // ----------------------------------------------------------------------- + // Control methods + // ----------------------------------------------------------------------- + + /** + * Stops the intake motor immediately by cutting power to zero. + * Should be called whenever the intake is no longer needed to prevent + * unnecessary wear and to avoid accidentally ingesting extra artifacts. + */ public void stop() { - intakeMotor.stopMotor(); + intakeMotor.stopMotor(); // set motor power to 0 and coast to a stop } + + /** + * Runs the intake at full speed in the intaking direction ({@link #INTAKING_POWER}). + * Use this during active field collection when the robot drives over artifacts. + */ public void run() { - intakeMotor.set(INTAKING_POWER); + intakeMotor.set(INTAKING_POWER); // full-speed intake (pulls artifacts inward) } + + /** + * Runs the intake at reduced speed ({@link #INTAKING_POWER} × + * {@link #SLOW_MULTIPLIER}). Useful as a "passive hold" mode while the + * indexer positions an artifact that was just collected, preventing a second + * artifact from ramming in before the indexer is ready. + */ public void runSlow() { - intakeMotor.set(INTAKING_POWER * SLOW_MULTIPLIER); + intakeMotor.set(INTAKING_POWER * SLOW_MULTIPLIER); // half-speed intake } + + /** + * Runs the intake in reverse at full speed, ejecting artifacts back onto + * the field. Useful for clearing jams or returning incorrect pieces. + */ public void runBackwards() { + // Negating INTAKING_POWER flips the motor direction to "eject" intakeMotor.set(-INTAKING_POWER); } + + /** + * Runs the intake in reverse at reduced speed ({@code -INTAKING_POWER × + * SLOW_MULTIPLIER}). Provides a gentler eject for careful jam-clearing + * without flinging artifacts too far. + */ public void runBackwardsSlow() { - intakeMotor.set(-INTAKING_POWER * SLOW_MULTIPLIER); + intakeMotor.set(-INTAKING_POWER * SLOW_MULTIPLIER); // slow reverse eject } + + /** + * Sets the intake motor to an arbitrary power level. + * Useful for fine-tuning or for custom autonomous sequences that need a + * specific power value not covered by the preset methods above. + * + * @param newPower motor power in the range [-1.0, 1.0]; positive pushes + * artifacts out, negative pulls them in (given the motor + * convention set in {@link #INTAKING_POWER}) + */ public void setPower(double newPower) { - intakeMotor.set(newPower); + intakeMotor.set(newPower); // directly write the requested power to the motor } + + /** + * Returns the current power being applied to the intake motor. + * Used in telemetry to verify that the intake is running at the expected level. + * + * @return current motor power in the range [-1.0, 1.0] + */ public double getPower() { - return intakeMotor.get(); + return intakeMotor.get(); // read back the last commanded power from the motor wrapper } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Movement.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Movement.java index 550684380ba6..e30f09c02e4a 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Movement.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Movement.java @@ -12,51 +12,137 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; /** - * Represents the drivetrain. + * Movement controls the four-wheel mecanum drivetrain. + * + *

A mecanum drivetrain uses wheels with rollers set at 45° to the axle, which + * allows independent X, Y, and rotational motion. This class provides both a + * robot-centric (standard) and a field-centric teleop tick that translates + * driver stick inputs into per-wheel power values. + * + *

Motors are wired and configured as follows: + *

+ * All four motors use {@link DcMotor.ZeroPowerBehavior#BRAKE} so the robot stops + * crisply when the driver releases the sticks. */ public class Movement { - private final DcMotor leftFront, leftBack, rightFront, rightBack; + + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** Left-front mecanum wheel motor. */ + private final DcMotor leftFront; + + /** Left-rear mecanum wheel motor. */ + private final DcMotor leftBack; + + /** Right-front mecanum wheel motor. */ + private final DcMotor rightFront; + + /** Right-rear mecanum wheel motor. */ + private final DcMotor rightBack; + + /** Road Runner MecanumDrive reference used for field-centric heading reads + * and for updating the pose estimate via the localizer. */ private final MecanumDrive drive; - //private final IMU imu; - private final double STRAFE_MULTIPLIER = 1.0, ROTATION_MULTIPLIER = 0.8; + + // IMU is currently unused (commented out) because the bot heading is + // obtained from the Road Runner localizer instead. + // private final IMU imu; + + // ----------------------------------------------------------------------- + // Scaling constants + // ----------------------------------------------------------------------- + + /** Multiplier applied to both the forward/axial and strafe/lateral + * components. A value of 1.0 means full stick deflection produces + * full motor power in those axes. */ + private final double STRAFE_MULTIPLIER = 1.0; + + /** Multiplier applied to the rotational/yaw component. Reduced to 0.8 to + * make turning slightly less sensitive than strafing/driving. */ + private final double ROTATION_MULTIPLIER = 0.8; + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- /** - * Initializes a Movement instance. - * @param map {@link com.qualcomm.robotcore.hardware.HardwareMap} + * Initialises the Movement subsystem by fetching all four drive motors from + * the hardware map, configuring their directions, and setting their zero- + * power behavior to BRAKE. + * + * @param map the robot's hardware map, non-null + * @param drive the Road Runner MecanumDrive used to read heading for + * field-centric mode and to keep the pose estimate updated */ public Movement(@NonNull HardwareMap map, MecanumDrive drive){ - leftFront = map.get(DcMotor.class, "leftFront"); - leftBack = map.get(DcMotor.class, "leftBack"); + // Retrieve each motor by the name registered in the hardware configuration. + leftFront = map.get(DcMotor.class, "leftFront"); + leftBack = map.get(DcMotor.class, "leftBack"); rightFront = map.get(DcMotor.class, "rightFront"); - rightBack = map.get(DcMotor.class, "rightBack"); + rightBack = map.get(DcMotor.class, "rightBack"); - this.drive = drive; - //this.imu = drive.lazyImu.get(); + this.drive = drive; // store Road Runner drive for localizer access + // this.imu = drive.lazyImu.get(); // IMU path: currently disabled + // Reverse left-side motors so that all motors spin "forward" with a + // positive power value, given the physical wiring of this robot. leftFront.setDirection(DcMotorSimple.Direction.REVERSE); leftBack.setDirection(DcMotorSimple.Direction.REVERSE); rightFront.setDirection(DcMotorSimple.Direction.FORWARD); rightBack.setDirection(DcMotorSimple.Direction.FORWARD); + // Enable BRAKE mode on all motors so the robot holds its position when + // power is cut, instead of coasting unpredictably. leftFront.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); leftBack.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); rightFront.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); rightBack.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); } - // tick for teleop + // ----------------------------------------------------------------------- + // Teleop drive methods + // ----------------------------------------------------------------------- + /** + * Robot-centric mecanum drive tick called every loop iteration during teleop. + * + *

Translates three driver inputs (forward/axial, strafe/lateral, rotate/yaw) + * into individual wheel powers using the standard mecanum kinematics matrix. + * A "denominator" normalisation ensures at least one wheel always reaches + * full power when the driver pushes to the stick limit. + * + *

An additional {@code turnCorrection} term from the AprilTag aimer is + * blended into the yaw component to keep the robot pointed at the goal + * while the driver manoeuvres. + * + * @param leftStickX horizontal axis of the left stick (-1 = strafe left, +1 = strafe right) + * @param leftStickY vertical axis of the left stick (-1 = drive back, +1 = drive forward) + * @param rightStickX horizontal axis of the right stick(-1 = turn left, +1 = turn right) + * @param turnCorrection additional rotation correction from vision aiming (can be 0 to disable) + */ public void teleopTick(double leftStickX, double leftStickY, double rightStickX, double turnCorrection){ - double axial = leftStickY * STRAFE_MULTIPLIER; - double lateral = leftStickX * STRAFE_MULTIPLIER; - double yaw = rightStickX * ROTATION_MULTIPLIER + turnCorrection; + // Scale the axial (forward) and lateral (strafe) components by the multiplier. + double axial = leftStickY * STRAFE_MULTIPLIER; // forward/backward + double lateral = leftStickX * STRAFE_MULTIPLIER; // left/right strafe + // Blend the driver's rotation request with the auto-aim correction. + double yaw = rightStickX * ROTATION_MULTIPLIER + turnCorrection; - double leftFrontPower = axial + lateral + yaw; - double rightFrontPower = axial - lateral - yaw; - double leftBackPower = axial - lateral + yaw; - double rightBackPower = axial + lateral - yaw; + // Mecanum kinematics: each wheel's power = sum/difference of axial, lateral, yaw. + double leftFrontPower = axial + lateral + yaw; // front-left: all three add + double rightFrontPower = axial - lateral - yaw; // front-right: lateral & yaw subtract + double leftBackPower = axial - lateral + yaw; // back-left: lateral subtracts, yaw adds + double rightBackPower = axial + lateral - yaw; // back-right: yaw subtracts - // For smoother joystick movement + // Normalise: find the largest absolute wheel power. If it exceeds 1.0 + // we divide all powers by it to keep them in [-1, 1] while preserving + // their ratios (smooth joystick "feel"). double denominator = Math.max(1.0, Math.abs(axial) + Math.abs(lateral) + Math.abs(yaw)); leftFrontPower /= denominator; @@ -64,66 +150,116 @@ public void teleopTick(double leftStickX, double leftStickY, double rightStickX, leftBackPower /= denominator; rightBackPower /= denominator; + // Write the computed powers to the physical motors. leftFront.setPower(leftFrontPower); rightFront.setPower(rightFrontPower); leftBack.setPower(leftBackPower); rightBack.setPower(rightBackPower); } + /** + * Field-centric mecanum drive tick called every loop iteration during teleop. + * + *

NOTE: Field-centric mode is currently not fully functional with the + * AprilTag aiming system. The heading source (Road Runner localizer) + * must have been initialised/re-zeroed before calling this method. + * + *

In field-centric mode the driver's forward direction always corresponds + * to the physical field "north" regardless of the robot's heading. This is + * achieved by rotating the stick input vector by the negative of the robot's + * current heading angle. + * + * @param leftStickX horizontal axis of the left stick (field-relative strafe) + * @param leftStickY vertical axis of the left stick (field-relative forward) + * @param rightStickX horizontal axis of the right stick (rotation) + * @param turnCorrection additional rotation correction from vision aiming + * @param start when {@code true} resets the localizer pose to (0, 0, 0); + * typically mapped to the "Start" button on an Xbox-style + * controller to re-zero field orientation on demand + */ // NOTE DOESN'T WORK WITH APRILTAG RN public void teleopTickFieldCentric(double leftStickX, double leftStickY, double rightStickX, double turnCorrection, boolean start){ - double axial = leftStickY * STRAFE_MULTIPLIER; - double lateral = leftStickX * STRAFE_MULTIPLIER; - double yaw = rightStickX * ROTATION_MULTIPLIER + turnCorrection; + double axial = leftStickY * STRAFE_MULTIPLIER; // field-relative forward + double lateral = leftStickX * STRAFE_MULTIPLIER; // field-relative strafe + double yaw = rightStickX * ROTATION_MULTIPLIER + turnCorrection; + // If the driver requests a heading reset, zero the localizer pose so that + // the current robot orientation becomes the new field "forward". // This button choice was made so that it is hard to hit on accident, // it can be freely changed based on preference. // The equivalent button is start on Xbox-style controllers. if (start) { - drive.localizer.setPose(new Pose2d(0, 0, 0)); + drive.localizer.setPose(new Pose2d(0, 0, 0)); // re-zero field origin at current robot pose } - double botHeading = drive.localizer.getPose().heading.log();//imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS); + // Read the robot's heading angle (in radians) from the Road Runner localizer. + double botHeading = drive.localizer.getPose().heading.log(); // imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS); - // Rotate the movement direction counter to the bot's rotation + // Rotate the driver's input vector counter to the robot's heading so that + // "forward" on the stick always means "field north" regardless of robot orientation. double rotX = lateral * Math.cos(-botHeading) - axial * Math.sin(-botHeading); double rotY = lateral * Math.sin(-botHeading) + axial * Math.cos(-botHeading); + // Compensate for imperfect strafing (mecanum wheels tend to slip laterally; + // scaling by 1.1 empirically corrects for this offset on this robot). rotX = rotX * 1.1; // Counteract imperfect strafing + // Normalise: largest absolute component (or 1 if all are ≤ 1) to keep + // powers within [-1, 1] while maintaining their ratio. // Denominator is the largest motor power (absolute value) or 1 // This ensures all the powers maintain the same ratio, // but only if at least one is out of the range [-1, 1] double denominator = Math.max(Math.abs(rotY) + Math.abs(rotX) + Math.abs(yaw), 1); - double frontLeftPower = (rotY + rotX + yaw) / denominator; - double backLeftPower = (rotY - rotX + yaw) / denominator; + double frontLeftPower = (rotY + rotX + yaw) / denominator; + double backLeftPower = (rotY - rotX + yaw) / denominator; double frontRightPower = (rotY - rotX - yaw) / denominator; - double backRightPower = (rotY + rotX - yaw) / denominator; + double backRightPower = (rotY + rotX - yaw) / denominator; + // Write field-centric powers to each wheel. leftFront.setPower(frontLeftPower); leftBack.setPower(backLeftPower); rightFront.setPower(frontRightPower); rightBack.setPower(backRightPower); } + // ----------------------------------------------------------------------- + // Motor accessors (used by autonomous and diagnostics) + // ----------------------------------------------------------------------- + + /** @return the left-front drive motor hardware object */ public DcMotor getLeftFront() { return leftFront; } + /** @return the left-rear drive motor hardware object */ public DcMotor getLeftBack() { return leftBack; } + /** @return the right-front drive motor hardware object */ public DcMotor getRightFront() { return rightFront; } + /** @return the right-rear drive motor hardware object */ public DcMotor getRightBack() { return rightBack; } + // ----------------------------------------------------------------------- + // Pose helper + // ----------------------------------------------------------------------- + + /** + * Updates the Road Runner localizer's stored pose estimate. + * Useful after a known field event (e.g. the robot presses against a wall) + * that allows a hard reset of the position estimate without performing a + * full relocalization scan. + * + * @param newPose the new absolute pose to assign to the localizer + */ public void setPose(Pose2d newPose) { - drive.localizer.setPose(newPose); + drive.localizer.setPose(newPose); // propagate the new pose into the Road Runner localizer } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Outtake.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Outtake.java index 7b9415abcb29..8e5420d18336 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Outtake.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Outtake.java @@ -8,44 +8,149 @@ import com.qualcomm.robotcore.hardware.HardwareMap; /** - * POWER mode → set(x) sets power 0–1 - * RPM mode → set(x) sets RPM target and uses PIDF control + * Outtake manages the dual-flywheel shooter that launches game artifacts toward + * the scoring goal. + * + *

Two operating modes are supported: + *

+ * + *

The shooter consists of two motors wired in opposite polarities (one inverted, + * one not) so they spin in the same physical direction while facing each other. + * + *

A pre-computed range-to-RPM look-up table ({@link #REGRESSION_DATA}) along + * with cubic polynomial ({@link #cubicRegressionRPM}) and linear-interpolation + * helpers let the robot calculate the exact shooter speed needed for a measured + * target distance. + * + *

Annotated with {@link Config} so all gains and tunables are editable live + * in FTC Dashboard. */ - @Config public class Outtake { + // ----------------------------------------------------------------------- + // Mode enum + // ----------------------------------------------------------------------- + + /** + * Selects open-loop (direct power) vs closed-loop (PIDF RPM) control. + */ public enum Mode { + /** Open-loop: {@link #set(double)} writes power 0–1 directly to the motors. */ POWER, + /** Closed-loop: {@link #set(double)} sets a target RPM tracked by PIDF each cycle. */ RPM } + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** Primary flywheel motor, mapped as {@code "outtake"}. Inverted so both + * flywheels spin in the same physical direction. */ private final MotorEx shooter; + + /** Secondary flywheel motor, mapped as {@code "outtake-2"}. Not inverted; + * combined with the primary motor its shaft faces the opposite way so both + * contact surfaces move in the same direction against the artifact. */ private final MotorEx shooter2; - // FTCLib PID controller (P, I, D only) + // ----------------------------------------------------------------------- + // Control + // ----------------------------------------------------------------------- + + /** FTCLib PID controller used in RPM mode. Only P, I, D terms are managed + * here; the feedforward term {@link #f} is computed separately and added + * to the PID output. */ private final PIDController controller; - // Dashboard-tunable gains + // ----------------------------------------------------------------------- + // Dashboard-tunable PIDF gains + // ----------------------------------------------------------------------- + + /** Proportional gain for the RPM PID loop. Increase to react faster to + * RPM error; too high causes oscillation. */ public static double p = 0.000567; + + /** Integral gain for the RPM PID loop. Accumulates steady-state error over + * time. Currently 0 because feedforward alone eliminates most steady-state + * offset. */ public static double i = 0.0; + + /** Derivative gain for the RPM PID loop. Damps rapid RPM changes. + * Currently 0; may be tuned if oscillation is observed. */ public static double d = 0.0; + + /** Feedforward gain – approximately 1 / maxRPM, then hand-tuned. + * Multiplied by {@link #targetRPM} to give the approximate power needed + * to sustain that speed before the P term corrects any residual error. */ public static double f = 0.0002; // 1 / maxrpm and then tuned - // Mode + state + // ----------------------------------------------------------------------- + // State + // ----------------------------------------------------------------------- + + /** Current operating mode ({@link Mode#POWER} or {@link Mode#RPM}). */ public Mode mode; + + /** Last motor power written to the flywheels (0–1 in POWER mode; + * computed PIDF output in RPM mode). */ private double motorPower = 0.0; + + /** Target shooter speed in RPM (used only in {@link Mode#RPM}). + * Defaults to 2800 RPM, which is used when no AprilTag range is available. */ public static double targetRPM = 2800.0; // without seeing any tags + + /** Most recent measured flywheel speed in RPM, updated every call to + * {@link #periodic()}. */ private double currentRPM = 0.0; + /** Encoder ticks per motor output shaft revolution for the flywheel motors. + * Used to convert raw encoder velocity (ticks/s) to RPM. */ private final double TPR = 28.0; // encoder ticks per rotation + // ----------------------------------------------------------------------- + // Spin-up timing + // ----------------------------------------------------------------------- + + /** Minimum time in milliseconds that the RPM must stay within tolerance + * before {@link #inRange(double)} reports {@code true}. Prevents + * false "ready" signals from a single momentary on-target reading. */ public static double spinupInRangeMinTime = 200; // ms + + /** Hard timeout in milliseconds for the spin-up phase. If the flywheel + * has not reached the target within this window {@link #inRange(double)} + * returns {@code true} anyway so the robot doesn't stall indefinitely. */ public static double spinupMaxTime = 2750; // ms + + /** Timestamp (ms) when the RPM first entered the tolerance band, or -1 + * if it is currently outside tolerance or a new target was just set. */ private long inRangeStartTime = -1; + + /** Timestamp (ms) when the current spin-up sequence began, or -1 if no + * spin-up is currently in progress. */ private long spinupStartTime = -1; + + /** Minimum target RPM for the intake-pass-through scenario. Any target + * below this value is considered "stopped" for regression purposes. */ public static double INTAKE_MIN_RPM = 3500.0; + // ----------------------------------------------------------------------- + // Range-to-RPM look-up table (full resolution) + // ----------------------------------------------------------------------- + + /** + * Empirically measured data pairs of {range (inches), optimal shooter RPM}. + * Collected by shooting from each measured distance and recording the RPM + * that produced the best scoring rate. Used by the linear-interpolation + * regression helpers. + */ private static final double[][] REGRESSION_DATA = { {46.4, 3400}, {48.3, 3450}, @@ -78,6 +183,16 @@ public enum Mode { {116.6, 4700} }; + // ----------------------------------------------------------------------- + // Reduced look-up table (sparse key points only) + // ----------------------------------------------------------------------- + + /** + * A sparse subset of {@link #REGRESSION_DATA} containing only key boundary + * points. Used by {@link #linearInterpolationRegressionReducedRPM(double)} + * to provide a faster (but less accurate) interpolation when full resolution + * is unnecessary. + */ private static final double[][] REGRESSION_DATA_REDUCED = { {46.4, 3400}, {61.7, 3660}, @@ -86,139 +201,308 @@ public enum Mode { {116.6, 4700} }; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an Outtake instance, initialises both flywheel motors, and + * sets up the PID controller with the current gain values. + * + * @param hardwareMap the robot hardware map used to resolve motor config names + * @param mode initial operating mode ({@link Mode#POWER} or {@link Mode#RPM}) + */ public Outtake(HardwareMap hardwareMap, Mode mode) { + // Retrieve and configure the primary flywheel motor. + // Inverted = true so that this motor spins in the correct physical direction. shooter = new MotorEx(hardwareMap, "outtake"); shooter.setInverted(true); + + // Retrieve and configure the secondary flywheel motor. + // Not inverted – when combined with the primary (facing opposite), + // both contact surfaces push the artifact in the same direction. shooter2 = new MotorEx(hardwareMap, "outtake-2"); shooter2.setInverted(false); + this.mode = mode; // store the requested operating mode - this.mode = mode; + // Initialise the PID controller with the dashboard-tunable gains. controller = new PIDController(p, i, d); } + // ----------------------------------------------------------------------- + // Public API + // ----------------------------------------------------------------------- + + /** + * Stops both flywheel motors immediately and resets internal state. + * Sets {@link #motorPower} and {@link #targetRPM} to 0 so that + * subsequent PIDF calls do not fight to restart the flywheels. + */ public void stop() { - shooter.stopMotor(); - shooter2.stopMotor(); - motorPower = 0.0; - targetRPM = 0.0; + shooter.stopMotor(); // cut power to primary flywheel + shooter2.stopMotor(); // cut power to secondary flywheel + motorPower = 0.0; // clear the cached power so it isn't re-applied + targetRPM = 0.0; // clear the RPM target so PIDF stays off } - /** Unified setter */ + /** + * Unified setter for both operating modes. + * + *

+ * + * @param x desired power (POWER mode) or desired RPM (RPM mode) + */ public void set(double x) { if (mode == Mode.POWER) { + // Clamp power to [0, 1] so the motors are never commanded backward + // (flywheels should only spin in one direction). motorPower = clamp(x, 0.0, 1.0); } else { + // RPM mode: if the target changes by more than 25 RPM we treat it + // as a new spin-up request and reset the timing state. if (Math.abs(x - targetRPM) > 25) { - spinupStartTime = -1; - inRangeStartTime = -1; + spinupStartTime = -1; // mark spin-up as not started + inRangeStartTime = -1; // mark "in range" timer as not started } - targetRPM = x; + targetRPM = x; // store the new target RPM for use in periodic() } } + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + /** + * Returns the most recently measured flywheel speed in RPM. + * Updated each call to {@link #periodic()}. + * + * @return current flywheel RPM (may lag real speed by one control cycle) + */ public double getRPM() { return currentRPM; } + + /** + * Returns the currently commanded target RPM. + * + * @return target RPM (0 when stopped) + */ public double getTargetRPM() { return targetRPM; } + + /** + * Returns the last motor power written to the flywheels. + * + * @return motor power in the range [-1.0, 1.0] (POWER mode) or the PIDF + * output clamped to [-1.0, 1.0] (RPM mode) + */ public double getPower() { return motorPower; } + // ----------------------------------------------------------------------- + // Control loop + // ----------------------------------------------------------------------- + + /** + * Must be called once per robot loop iteration. Reads the current RPM + * from the encoder, computes PIDF output in RPM mode (or open-loop in + * POWER mode), and writes the resulting power to both flywheel motors. + */ public void periodic() { - // Update current RPM from motor encoder + // Compute current RPM from the primary motor's raw encoder velocity. + // getVelocity() returns ticks/second; divide by TPR and multiply by 60 + // to get rotations per minute. currentRPM = shooter.getVelocity() / TPR * 60.0; if (mode == Mode.POWER) { - // Open-loop mode + // Open-loop path: simply write the stored motorPower to both motors. shooter.set(motorPower); shooter2.set(motorPower); - return; + return; // skip PIDF calculation entirely } - // Update PID gains live from dashboard + // Sync PID gains from dashboard-editable static fields so changes take + // effect immediately without redeploying code. controller.setPID(p, i, d); - // Compute PID term + // PID term: controller compares current RPM to target and outputs a + // correction value (positive = need to speed up, negative = overspun). double pid = controller.calculate(currentRPM, targetRPM); - // Compute feedforward term from earlier code + // Feedforward term: gives the motor a head-start power proportional to + // the target RPM, reducing the burden on the P term. double ff = targetRPM * f; - // Combined PIDF output + // Sum PID and feedforward for the combined PIDF output. motorPower = pid + ff; - // Constrain power + // Clamp combined output to valid motor power range. motorPower = clamp(motorPower, -1.0, 1.0); + // Apply the same power to both flywheels so they stay synchronised. shooter.set(motorPower); shooter2.set(motorPower); } + // ----------------------------------------------------------------------- + // Range-to-RPM regression helpers + // ----------------------------------------------------------------------- + + /** + * Quartic polynomial regression mapping target range (inches) to shooter RPM. + * Derived by fitting a degree-4 polynomial to the full empirical data set. + * Kept for reference; the cubic version is currently used in production. + * + * @param range distance from robot to goal in inches + * @return predicted optimal shooter RPM for that range + */ private double quarticRegressionRPM(double range) { + // Evaluate: -0.000297337·r⁴ + 0.0958661·r³ - 11.09971·r² + 562.06918·r - 6981.95351 return range * (range * (range * (range * -0.000297337 + 0.0958661) - 11.09971) + 562.06918) - 6981.95351; } + /** + * Cubic polynomial regression mapping target range (inches) to shooter RPM. + * Fits a degree-3 polynomial to the empirical data set. + * This is the function currently used by {@link #getRegressionRPM(double)}. + * + * @param range distance from robot to goal in inches + * @return predicted optimal shooter RPM for that range + */ private double cubicRegressionRPM(double range) { + // Evaluate: -0.000281754·r³ + 0.228245·r² - 13.14333·r + 3623.28132 return range * (range * (range * -0.000281754 + 0.228245) - 13.14333) + 3623.28132; } + /** + * Performs piecewise linear interpolation over a given data table to map + * range (x) to RPM (y). Out-of-bounds ranges are clamped to the nearest + * end segment (extrapolation via the first/last interval). + * + * @param range distance from robot to goal in inches + * @param data two-column array of {range, RPM} key points, sorted ascending + * by range + * @return linearly-interpolated RPM value + */ private double linearInterpolation(double range, double[][] data) { double sum = 0; for (int i = 0; i < data.length - 1; i++) { + // For the first segment, extend left to -∞; for the last, extend right to +∞. double min = i == 0 ? Double.MIN_VALUE : data[i][0]; double max = i == data.length - 2 ? Double.MAX_VALUE : data[i + 1][0]; + // If range falls in this interval, compute and accumulate the interpolated value. if (range >= min && range < max) sum += - (range - data[i][0]) / (data[i + 1][0] - data[i][0]) * - (data[i + 1][1] - data[i][1]) + data[i][1]; + (range - data[i][0]) / (data[i + 1][0] - data[i][0]) * // fractional position in interval + (data[i + 1][1] - data[i][1]) + data[i][1]; // interpolated RPM } - return sum; + return sum; // only one segment fires, so sum equals that segment's result } + /** + * Linear interpolation using the full {@link #REGRESSION_DATA} table. + * + * @param range distance from robot to goal in inches + * @return interpolated optimal shooter RPM + */ private double linearInterpolationRegressionRPM(double range) { return linearInterpolation(range, REGRESSION_DATA); } + /** + * Linear interpolation using the sparse {@link #REGRESSION_DATA_REDUCED} table. + * Faster to compute than the full-table version but less precise between key points. + * + * @param range distance from robot to goal in inches + * @return interpolated optimal shooter RPM (reduced accuracy) + */ private double linearInterpolationRegressionReducedRPM(double range) { return linearInterpolation(range, REGRESSION_DATA_REDUCED); } + /** + * Public facade: returns the recommended shooter RPM for a given target range. + * Validates the range and falls back to {@link #INTAKE_MIN_RPM} for invalid + * or zero-distance inputs. Currently delegates to the cubic polynomial. + * + * @param range distance from robot to goal in inches; must be > 0 and finite + * @return optimal shooter RPM, or {@link #INTAKE_MIN_RPM} if range is invalid + */ public double getRegressionRPM(double range) { if (Double.isNaN(range) || range <= 0) { + // No valid range data available (tag not visible, or called too early); + // return a safe default RPM so the shooter is still usable. return INTAKE_MIN_RPM; } - // Just use one of the three functions above + // Delegate to the cubic polynomial regression (best single-function fit). return cubicRegressionRPM(range); } - // Within the range and has been in range for spinupInRangeMinTime + // ----------------------------------------------------------------------- + // Spin-up readiness check + // ----------------------------------------------------------------------- + + /** + * Returns {@code true} when the flywheels are considered ready to shoot. + * + *

"Ready" means either: + *

+ * + *

After returning {@code true} all timing state is reset so the next + * call to {@link #set(double)} starts a fresh spin-up sequence. + * + * @param tolerance allowable RPM deviation from {@link #targetRPM} to be + * considered "in range" (e.g. 50 RPM or 100 RPM) + * @return {@code true} if the shooter is ready to fire; {@code false} if + * still spinning up + */ public boolean inRange(double tolerance) { long currentTime = System.currentTimeMillis(); + + // Initialise the spin-up start timestamp on the first call after a new + // target was set (indicated by spinupStartTime == -1). if (spinupStartTime == -1) { spinupStartTime = currentTime; } + // Check whether the measured RPM is within the specified tolerance band. boolean withinTolerance = Math.abs(currentRPM - targetRPM) <= tolerance; if (withinTolerance) { + // Start the sustained-in-range timer if this is the first in-range sample. if (inRangeStartTime == -1) inRangeStartTime = currentTime; } else { - inRangeStartTime = -1; + // RPM left the tolerance band; reset the sustained timer. + inRangeStartTime = -1; } + // Has the RPM been continuously in range for the minimum required duration? boolean inRangeLongEnough = inRangeStartTime >= 0 && (currentTime - inRangeStartTime) >= spinupInRangeMinTime; + + // Has the overall spin-up exceeded the maximum allowed time (safety valve)? boolean spunPastMaxTime = (currentTime - spinupStartTime) >= spinupMaxTime; if (inRangeLongEnough || spunPastMaxTime) { + // Reset timing state so the next spin-up begins clean. spinupStartTime = -1; inRangeStartTime = -1; - return true; + return true; // shooter is ready (or timed out – fire anyway) } - return false; + return false; // still spinning up; caller should keep waiting } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Turret.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Turret.java index 25f5f96f4ed4..26c4a972bc23 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Turret.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/Turret.java @@ -4,25 +4,106 @@ import com.qualcomm.robotcore.hardware.DcMotor; import com.qualcomm.robotcore.hardware.HardwareMap; +/** + * Turret models a rotational (panning) platform driven by a single DC motor. + * + *

The turret can be used to aim a sensor or shooter by rotating it to a + * desired angle. Angle calculation uses a ticks-per-degree conversion factor + * ({@link #TICKS_TO_DEGREES}) and a zero-position offset ({@link #ZERO_OFFSET}) + * that compensate for the motor's encoder starting point and any mechanical + * offset between "encoder zero" and the physical zero of the turret. + * + *

The current angle is always wrapped into the [0, 360) range to avoid + * ambiguity about "which revolution" the turret is on. + * + *

Annotated with {@link Config} so {@link #TICKS_TO_DEGREES} and + * {@link #ZERO_OFFSET} can be calibrated live in FTC Dashboard. + */ @Config public class Turret { + + // ----------------------------------------------------------------------- + // Dashboard-tunable calibration constants + // ----------------------------------------------------------------------- + + /** Conversion factor from raw encoder ticks to degrees of turret rotation. + * Set to 0 by default; must be measured and configured for the specific + * gear ratio and encoder used on this robot. */ public static double TICKS_TO_DEGREES = 0; + + /** Angular offset (degrees) added to the converted tick value to align + * the encoder's zero position with the desired physical zero of the turret. + * A positive offset rotates the "zero" clockwise. */ public static double ZERO_OFFSET = 0; + + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** The DC motor that drives the turret rotation mechanism. + * Mapped to hardware config name {@code "TurretMotor"}. */ private final DcMotor motor; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates a Turret instance, retrieves the motor from the hardware map, + * and configures it to BRAKE when power is zero (so the turret holds its + * position without drifting when no correction is applied). + * + * @param hardwareMap the robot's hardware map used to look up the motor + * registered under the config name {@code "TurretMotor"} + */ public Turret(HardwareMap hardwareMap) { + // Retrieve the turret motor from the hardware map. motor = hardwareMap.get(DcMotor.class, "TurretMotor"); + + // BRAKE mode holds the turret in place when power = 0, preventing wind-up. motor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); - // May be needed? + + // Motor direction reversal may be needed depending on how the motor is mounted; + // uncomment the line below and adjust if the turret spins the wrong way. // motor.setDirection(DcMotorSimple.Direction.REVERSE); } + // ----------------------------------------------------------------------- + // Public API + // ----------------------------------------------------------------------- + + /** + * Returns the current turret angle in degrees, wrapped to [0, 360). + * + *

The raw encoder tick count is first multiplied by {@link #TICKS_TO_DEGREES} + * to convert to degrees, then {@link #ZERO_OFFSET} is added to correct for + * any mechanical misalignment. Finally the result is wrapped with a + * positive-modulo operation so that the angle is always in [0, 360). + * + *

Note: Java's {@code %} operator returns a negative result for + * negative operands (e.g., {@code -2 % 5 == -2}), so the double-modulo + * pattern {@code ((x % 360) + 360) % 360} is used to guarantee a positive + * result. + * + * @return current turret angle in degrees, range [0, 360) + */ public double getCurrentAngle() { + // Convert encoder ticks to degrees and apply the zero-point offset. double curAngle = motor.getCurrentPosition() * TICKS_TO_DEGREES + ZERO_OFFSET; // In Java -2 % 5 = -2, not 3 + // Wrap into [0, 360): add 360 first then take mod again to handle negative values. return ((curAngle % 360) + 360) % 360; } + + /** + * Sets the raw power applied to the turret motor. + * A positive value rotates in one direction; a negative value reverses it. + * Callers are responsible for implementing closed-loop angle control on + * top of this method. + * + * @param power motor power in the range [-1.0, 1.0] + */ public void setPower(double power) { - motor.setPower(power); + motor.setPower(power); // directly apply power to the turret motor } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/BetterCRControl.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/BetterCRControl.java index cc2a08680f16..65cacde042f8 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/BetterCRControl.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/BetterCRControl.java @@ -1,80 +1,212 @@ package org.firstinspires.ftc.teamcode.subsystems.indexerUtil; + import com.acmerobotics.dashboard.config.Config; import com.qualcomm.robotcore.hardware.CRServo; import com.qualcomm.robotcore.hardware.AnalogInput; import com.qualcomm.robotcore.util.ElapsedTime; +/** + * BetterCRControl is a PID position controller for a continuous-rotation servo + * equipped with an analog (potentiometer-style) encoder. + * + *

Unlike a standard servo that accepts absolute position commands, + * a continuous-rotation (CR) servo must be driven by open-loop power signals. + * This class closes the position loop manually: + *

    + *
  1. Read the encoder voltage and convert it to an angle (0–360°).
  2. + *
  3. Compute angular error between the target and current angle.
  4. + *
  5. Run a PID calculation to determine a corrective power.
  6. + *
  7. Apply a hold power when within the deadband to resist disturbances.
  8. + *
+ * + *

Note: This class is an older/alternative implementation. + * {@link CRServoPositionControl} is currently used in production. + * + *

Annotated with {@link Config} so all gains and thresholds can be tuned + * live from the FTC Dashboard. + */ @Config public class BetterCRControl { + + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** The continuous-rotation servo being controlled. */ private final CRServo crServo; + + /** The analog encoder attached to the servo shaft. + * Provides a 0–3.2 V signal proportional to the current angle. */ private final AnalogInput encoder; + // ----------------------------------------------------------------------- + // Dashboard-tunable PID gains + // ----------------------------------------------------------------------- + + /** Proportional gain. Scales the error (degrees) directly into power. + * Higher values give faster response but increase overshoot risk. */ public static double kp = 0.45; + + /** Integral gain. Accumulates error over time to eliminate steady-state + * offset. Currently 0; enable only if there is noticeable steady-state + * positional drift. */ public static double ki = 0.0; + + /** Derivative gain. Damps rapid changes in error to reduce overshoot. + * A value of 0.04 provides light damping. */ public static double kd = 0.04; + // ----------------------------------------------------------------------- + // Dashboard-tunable thresholds + // ----------------------------------------------------------------------- + + /** Angular error (degrees) below which the servo is considered "on target". + * Within this band a small hold power is applied instead of running + * the full PID loop, preventing high-frequency chatter. */ public static double deadband = 3; // degrees + + /** Minimum absolute power applied when the error exceeds the deadband. + * Overcomes static friction (stiction) in the servo gearbox. */ public static double minPower = 0.08; + + /** Power applied while within the deadband to resist external disturbances + * (e.g., field elements pressing against the indexer). Applied in the + * direction of the last non-zero error to act as a light restoring force. */ public static double holdPower = 0.05; + // ----------------------------------------------------------------------- + // Internal PID state + // ----------------------------------------------------------------------- + + /** Accumulated error × dt (integral term numerator). */ private double integral = 0.0; + + /** Error from the previous loop iteration; used to compute the derivative. */ private double lastError = 0.0; + + /** Smoothed / filtered voltage reading from the encoder. + * Uses an exponential moving average to reduce noise. */ private double filteredVoltage = 0; + /** Elapsed-time timer used to measure the dt between consecutive + * {@link #moveToAngle(double)} calls for the integral and derivative terms. */ private ElapsedTime timer = new ElapsedTime(); + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates a BetterCRControl instance and resets the delta-time timer. + * + * @param servo the continuous-rotation servo to control + * @param encoder the analog encoder attached to the servo output shaft + */ public BetterCRControl(CRServo servo, AnalogInput encoder) { - this.crServo = servo; - this.encoder = encoder; - timer.reset(); + this.crServo = servo; // store servo reference + this.encoder = encoder; // store encoder reference + timer.reset(); // start the dt timer from zero } + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + /** + * Returns a smoothed encoder voltage using a fixed-coefficient exponential + * moving average (EMA): {@code new = 0.6 × old + 0.4 × raw}. + * This reduces high-frequency ADC noise without introducing significant + * latency into the position feedback. + * + * @return filtered encoder voltage in volts + */ private double getFilteredVoltage() { + // Blend the previous filtered value (60 %) with the new raw reading (40 %). filteredVoltage = 0.6 * filteredVoltage + 0.4 * encoder.getVoltage(); return filteredVoltage; } + /** + * Converts the analog encoder voltage to a shaft angle in degrees. + * + *

The encoder produces a 0–3.2 V signal over one full 360° revolution, + * so {@code angle = (voltage / 3.2) × 360}. + * + * @param v encoder voltage in volts (expected range: 0–3.2) + * @return corresponding shaft angle in degrees (0–360) + */ private double voltageToAngle(double v) { - return (v / 3.2) * 360.0; + return (v / 3.2) * 360.0; // linear mapping: 3.2 V → 360°, 0 V → 0° } + /** + * Wraps an angle into the range (-180, 180] so that the shortest-path error + * direction is always chosen. + * + * @param angle raw angle or error in degrees + * @return equivalent angle in (-180, 180] + */ private double wrapDegrees(double angle) { - angle %= 360; - if (angle > 180) angle -= 360; - if (angle < -180) angle += 360; + angle %= 360; // reduce to (-360, 360) + if (angle > 180) angle -= 360; // fold [180, 360) to [-180, 0) + if (angle < -180) angle += 360; // fold (-360, -180) to (0, 180] return angle; } + // ----------------------------------------------------------------------- + // Public control method + // ----------------------------------------------------------------------- + + /** + * Commands the servo to move toward the given target angle, using a PID + * controller to compute the required power. + * + *

Call this method every loop iteration while position control is active. + * The servo will move toward {@code target} and settle when the error falls + * within the {@link #deadband}. + * + * @param target desired shaft angle in degrees (wrapped internally to [-180, 180]) + */ public void moveToAngle(double target) { + // Read current angle from the (filtered) encoder. double current = voltageToAngle(getFilteredVoltage()); + + // Compute the shortest-path error from current to target. double error = wrapDegrees(target - current); - // Hold state behavior + // ---- Deadband / hold phase ---- if (Math.abs(error) < deadband) { + // Close enough: apply a small hold power in the direction of the last + // recorded error to resist backdrive forces. crServo.setPower(holdPower * Math.signum(lastError)); - lastError = error; - return; + lastError = error; // update for next iteration + return; // skip PID computation; we're within tolerance } - double dt = timer.seconds(); - timer.reset(); - if (dt < 0.0001) dt = 0.0001; + // ---- Full PID phase ---- + double dt = timer.seconds(); // elapsed time since the last call (seconds) + timer.reset(); // restart the timer for the next iteration + if (dt < 0.0001) dt = 0.0001; // guard against near-zero dt (first call or very fast loop) + // Accumulate error for the integral term and clamp to prevent wind-up. integral += error * dt; - integral = Math.max(-2, Math.min(2, integral)); + integral = Math.max(-2, Math.min(2, integral)); // clamp integral to [-2, 2] + // Derivative: rate of change of error since the previous call. double derivative = (error - lastError) / dt; + // Combine PID terms. double output = kp * error + ki * integral + kd * derivative; - // Minimum power to break stiction + // Enforce a minimum power to overcome static friction (stiction). if (Math.abs(output) < minPower) - output = minPower * Math.signum(output); + output = minPower * Math.signum(output); // preserve direction but boost magnitude + // Clamp final output to valid servo power range [-1, 1]. output = Math.max(-1.0, Math.min(1.0, output)); - crServo.setPower(output); + crServo.setPower(output); // apply computed power to the servo - lastError = error; + lastError = error; // save current error for next derivative calculation } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/CRServoPositionControl.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/CRServoPositionControl.java index 17de1780cf56..7a85b740d16c 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/CRServoPositionControl.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/CRServoPositionControl.java @@ -4,74 +4,206 @@ import com.qualcomm.robotcore.hardware.CRServo; import com.qualcomm.robotcore.hardware.AnalogInput; +/** + * CRServoPositionControl implements closed-loop angular position control for a + * continuous-rotation (CR) servo that has an analog (potentiometer-style) absolute + * encoder attached to its output shaft. + * + *

A CR servo does not natively accept position commands; it only responds to + * a −1 to +1 power signal. This class wraps the servo with: + *

    + *
  1. An angle tracking system that converts the encoder voltage to a + * continuous (unwrapped) angle, accumulating full rotations so the servo + * can be moved multiple revolutions without ambiguity.
  2. + *
  3. A PD + static-friction compensation controller that drives the + * servo to a commanded target angle and holds it there.
  4. + *
  5. An open-loop override mode used when the indexer needs to spin + * freely at a fixed power (e.g., during the "full blast" outtake).
  6. + *
  7. A loaded/unloaded gain switching feature that automatically + * applies different gains depending on whether artifacts are inside the + * indexer drum (heavier load needs stronger correction).
  8. + *
+ * + *

Annotated with {@link Config} so all dashboard fields are editable live + * in the FTC Dashboard. + */ @Config public class CRServoPositionControl { - // general constants + // ----------------------------------------------------------------------- + // General hardware / encoder constants + // ----------------------------------------------------------------------- + + /** Maximum voltage output of the analog encoder (volts). + * Corresponds to one full 360° revolution. Calibrated to 3.26 V. */ public static double maxVoltage = 3.26; + + /** Degrees in one full encoder revolution; always 360° for a rotary encoder. */ public static double degreesPerRev = 360.0; - // gains + // ----------------------------------------------------------------------- + // Active (runtime) PD gains + // ----------------------------------------------------------------------- + + /** Proportional gain: error (degrees) × kP = base output power. + * Higher values close the loop faster but risk oscillation. */ public static double kP = 0.004; + + /** Integral gain. Currently 0; enable only if steady-state offset persists + * under load. */ public static double kI = 0.0; + + /** Derivative gain: angular velocity (deg/s) × kD is subtracted from the + * output to damp overshoot. Currently 0. */ public static double kD = 0.0; + + /** Static friction (stiction) compensation: minimum power magnitude applied + * when the controller output would otherwise be too small to overcome + * gearbox friction and start moving the servo. */ public static double kS = 0.07; + // ----------------------------------------------------------------------- + // Power and stiffness limits + // ----------------------------------------------------------------------- + + /** Maximum absolute power that will be commanded to the servo. + * Clamps the controller output to prevent mechanical damage. */ public static double maxPower = 1.0; - public static double stiffnessGain = 1.0; //1.0 is normal behavior - // deadbands + /** Scales the proportional error term. 1.0 = normal behaviour. + * Can be increased for a stiffer position hold, e.g., when the indexer + * drum is under heavy load from the weight of three artifacts. */ + public static double stiffnessGain = 1.0; // 1.0 is normal behavior + + // ----------------------------------------------------------------------- + // Deadband + // ----------------------------------------------------------------------- + + /** Angular deadband in degrees: if |error| < deadbandDeg the servo is + * turned off (power = 0). Prevents constant small oscillations while + * the indexer is "close enough" to its target. */ public static double deadbandDeg = 6.0; + + /** Rotation-direction preference for {@link #moveToAngle(double)}. + * {@code true} = always move clockwise (positive angle increase); + * {@code false} = always move counter-clockwise. */ public static boolean rotateClockwise = true; - // Preset gains + // ----------------------------------------------------------------------- + // Preset gain sets – switched automatically based on load state + // ----------------------------------------------------------------------- + + // Unloaded preset (no artifacts in drum) – lighter gains suffice. public static double unloaded_kP = 0.004; public static double unloaded_kI = 0.0; public static double unloaded_kD = 0.0; public static double unloaded_kS = 0.08; + // Loaded preset (one or more artifacts in drum) – stronger integral and hold. public static double loaded_kP = 0.004; public static double loaded_kI = 0.00009; public static double loaded_kD = 0.0; public static double loaded_kS = 0.08; + // ----------------------------------------------------------------------- + // Internal state + // ----------------------------------------------------------------------- + + /** Whether the drum currently contains at least one artifact. + * Used to select between loaded and unloaded gain presets. */ private boolean loaded = false; - private boolean manualOverride = false; // when true, update() does nothing (open-loop control active) - // hardware + /** When {@code true} the {@link #update()} method is bypassed and the servo + * is running on a raw power command from {@link #setOpenLoopPower(double)}. + * Used during the "full blast" indexer spin. */ + private boolean manualOverride = false; + + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** The continuous-rotation servo hardware object. */ private final CRServo servo; + + /** The analog encoder reporting the servo's absolute shaft position. */ private final AnalogInput encoder; - // state + // ----------------------------------------------------------------------- + // Angle tracking state + // ----------------------------------------------------------------------- + + /** Last wrapped (0–360°) encoder angle, used to detect and accumulate + * full-revolution wrap-arounds for continuous angle tracking. */ private double lastWrappedDeg; + + /** Continuously-accumulated shaft angle (may exceed 360° or be negative). + * Monotonically increasing/decreasing across multiple revolutions so the + * PD target can be set without ambiguity. */ private double continuousDeg; + + /** The target angle (continuous units, not wrapped) that the controller is + * driving toward. */ private double targetDeg; + // ----------------------------------------------------------------------- + // Derivative calculation state + // ----------------------------------------------------------------------- + + /** Shaft angle at the time of the last derivative calculation (degrees). */ private double lastAngleDeg = 0.0; + + /** Timestamp (nanoseconds) of the last derivative calculation. + * Used to compute dt in seconds for the velocity calculation. */ private long lastTimeNs = 0; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates a CRServoPositionControl instance. Reads the initial encoder angle + * and uses it as the starting point for both the continuous-angle tracker and + * the initial target, so the servo holds its power-on position immediately. + * + * @param servo the CR servo hardware object + * @param encoder the analog encoder attached to the servo output shaft + */ public CRServoPositionControl(CRServo servo, AnalogInput encoder) { - this.servo = servo; + this.servo = servo; this.encoder = encoder; - double initial = getWrappedAngle(); - lastWrappedDeg = initial; - continuousDeg = initial; - targetDeg = initial; + // Bootstrap the continuous-angle tracker from the current raw encoder reading. + double initial = getWrappedAngle(); // read actual shaft angle at startup + lastWrappedDeg = initial; // store as the "previous" wrapped angle (no delta yet) + continuousDeg = initial; // initialise the continuous tracker at the current position + targetDeg = initial; // hold the current position until explicitly commanded - lastAngleDeg = continuousDeg; - lastTimeNs = System.nanoTime(); + lastAngleDeg = continuousDeg; // seed derivative state with current position + lastTimeNs = System.nanoTime(); // start the derivative timestamp } - //mroe balls is more gains + // ----------------------------------------------------------------------- + // Load state & gain switching + // ----------------------------------------------------------------------- + + /** + * Notifies the controller whether the indexer drum is loaded with artifacts. + * Automatically switches between the loaded and unloaded PD gain presets. + * + * @param hasBalls {@code true} if one or more artifacts are currently in + * the drum; {@code false} when the drum is empty + */ + // more balls is more gains public void setLoaded(boolean hasBalls) { this.loaded = hasBalls; if (hasBalls) { + // Loaded: apply heavier gains to hold against artifact weight. kP = loaded_kP; kI = loaded_kI; kD = loaded_kD; kS = loaded_kS; } else { + // Unloaded: revert to lighter gains for snappier, lower-current motion. kP = unloaded_kP; kI = unloaded_kI; kD = unloaded_kD; @@ -79,113 +211,254 @@ public void setLoaded(boolean hasBalls) { } } + /** + * Returns whether the drum is currently in the "loaded" state. + * + * @return {@code true} if loaded gains are active + */ public boolean isLoaded() { return loaded; } + // ----------------------------------------------------------------------- + // Open-loop override + // ----------------------------------------------------------------------- + + /** + * Switches to open-loop mode and sets a fixed raw power on the servo. + * The {@link #update()} method will be a no-op until {@link #clearOpenLoop()} + * is called. Used for the "full blast" indexer dump phase. + * + * @param power desired servo power in [-1.0, 1.0] + */ public void setOpenLoopPower(double power) { - manualOverride = true; - servo.setPower(clamp(power, -1.0, 1.0)); + manualOverride = true; // disable closed-loop update + servo.setPower(clamp(power, -1.0, 1.0)); // write clamped power directly to hardware } + /** + * Returns to closed-loop position control mode. + * Must be called after {@link #setOpenLoopPower(double)} to re-enable + * the PD controller in subsequent {@link #update()} calls. + */ public void clearOpenLoop() { - manualOverride = false; + manualOverride = false; // re-enable closed-loop update } + // ----------------------------------------------------------------------- + // Main control loop + // ----------------------------------------------------------------------- + + /** + * Runs one iteration of the position control loop. Must be called every + * robot loop iteration while position control is active. + * + *

If open-loop override is active ({@link #manualOverride} == true), + * this method is a no-op and the servo continues at the last power set by + * {@link #setOpenLoopPower(double)}. + * + *

Otherwise: + *

    + *
  1. Updates the continuous angle tracker.
  2. + *
  3. Computes the angular error.
  4. + *
  5. If within {@link #deadbandDeg}, stops the servo.
  6. + *
  7. Computes velocity and a PD output.
  8. + *
  9. Applies static-friction compensation and clamps to {@link #maxPower}.
  10. + *
  11. Writes the result to the servo.
  12. + *
+ */ public void update() { - if (manualOverride) return; // skip closed-loop when in override + if (manualOverride) return; // open-loop active: skip all closed-loop logic - updateContinuousAngle(); + updateContinuousAngle(); // integrate any wrap-arounds since the last call - double error = targetDeg - continuousDeg; + // Compute the signed angular error (continuous units). + double error = targetDeg - continuousDeg; double absErr = Math.abs(error); if (absErr < deadbandDeg) { + // Within deadband: stop the servo (no restoring force needed). servo.setPower(0); + // Reset derivative state so the next non-deadband motion starts clean. lastAngleDeg = continuousDeg; - lastTimeNs = System.nanoTime(); + lastTimeNs = System.nanoTime(); return; } - long now = System.nanoTime(); - double dt = (now - lastTimeNs) * 1e-9; - if (dt <= 0) dt = 1e-3; + // Compute time delta for derivative calculation. + long now = System.nanoTime(); + double dt = (now - lastTimeNs) * 1e-9; // convert nanoseconds to seconds + if (dt <= 0) dt = 1e-3; // guard against zero/negative dt - double velocity = (continuousDeg - lastAngleDeg) / dt; // deg/s + // Compute shaft angular velocity (degrees per second). + double velocity = (continuousDeg - lastAngleDeg) / dt; + // Update derivative state for next iteration. lastAngleDeg = continuousDeg; - lastTimeNs = now; + lastTimeNs = now; + // PD output: proportional term drives toward target; derivative damps velocity. + // stiffnessGain scales the P term to allow a stiffer hold under load. double output = kP * error * stiffnessGain - kD * velocity + kI * error; - // static friction compensation toward target + // Static friction compensation: if the output is in the same direction as + // the error (we need to move that way), ensure it is at least kS so the + // servo actually starts moving despite gearbox stiction. if (Math.signum(output) == Math.signum(error)) { double sign = Math.signum(output); output = sign * Math.max(Math.abs(output), kS); } - output = clamp(output, -maxPower, maxPower); - servo.setPower(output); + output = clamp(output, -maxPower, maxPower); // hard-limit to maxPower + servo.setPower(output); // write computed power to hardware } + // ----------------------------------------------------------------------- + // Position commands + // ----------------------------------------------------------------------- + + /** + * Commands the servo to move to a wrapped (0–360°) target angle using + * the preferred rotation direction ({@link #rotateClockwise}). + * + *

The wrapped angle is converted to a continuous-space target by choosing + * the delta that matches the rotation direction preference and adding it to + * the current continuous position. + * + * @param wrappedAngleDeg target angle in degrees, in the range [0, 360) + */ public void moveToAngle(double wrappedAngleDeg) { - updateContinuousAngle(); + updateContinuousAngle(); // ensure continuousDeg is current before computing the delta + // Current position in the wrapped [0, 360) space. double currentWrapped = mod(continuousDeg, 360.0); + // Shortest-path delta in [-180, 180]. double delta = wrappedAngleDeg - currentWrapped; if (delta > 180) delta -= 360; if (delta < -180) delta += 360; - // rotation direction - if (rotateClockwise && delta < 0) delta += 360; - if (!rotateClockwise && delta > 0) delta -= 360; + // Override delta to enforce the preferred rotation direction. + if ( rotateClockwise && delta < 0) delta += 360; // force clockwise (positive direction) + if (!rotateClockwise && delta > 0) delta -= 360; // force counter-clockwise - targetDeg = continuousDeg + delta; + targetDeg = continuousDeg + delta; // set the continuous-space target } + /** + * Moves the target by a relative amount from the current target angle. + * + * @param deltaDeg number of degrees to advance (positive = forward in + * the current rotation direction) + */ public void moveBy(double deltaDeg) { - targetDeg += deltaDeg; + targetDeg += deltaDeg; // simply offset the existing target } + /** + * Resets the continuous angle tracker and target to the current raw encoder + * reading, and stops the servo. Call this after manually repositioning the + * servo or recovering from a fault. + */ public void reset() { - double wrapped = getWrappedAngle(); - lastWrappedDeg = wrapped; - continuousDeg = wrapped; - targetDeg = wrapped; - servo.setPower(0); + double wrapped = getWrappedAngle(); // read the encoder's current physical position + lastWrappedDeg = wrapped; // seed the wrap-tracker + continuousDeg = wrapped; // reset the continuous accumulator + targetDeg = wrapped; // hold the current position (no motion) + servo.setPower(0); // cut power immediately } - //bencoder + // ----------------------------------------------------------------------- + // Internal angle tracking + // ----------------------------------------------------------------------- + + /** + * Updates the continuous-angle tracker by comparing the current wrapped + * encoder angle with the last recorded wrapped angle and accumulating any + * delta (including wrap-arounds) into {@link #continuousDeg}. + * + *

Wrap-arounds are detected when the raw delta exceeds ±180°; in that + * case 360° is added or subtracted to recover the true (small) motion delta. + */ + // bencoder private void updateContinuousAngle() { - double wrapped = getWrappedAngle(); - double delta = wrapped - lastWrappedDeg; + double wrapped = getWrappedAngle(); // current raw 0–360° reading + double delta = wrapped - lastWrappedDeg; // naïve delta (may include wrap error) - // unwrap - if (delta > 180) delta -= 360; - if (delta < -180) delta += 360; + // Unwrap: if the sensor jumped by > 180° it crossed the 0/360 boundary. + if (delta > 180) delta -= 360; // e.g. 359° → 5°: delta was +354, true delta is −6 + if (delta < -180) delta += 360; // e.g. 5° → 359°: delta was −354, true delta is +6 - continuousDeg += delta; - lastWrappedDeg = wrapped; + continuousDeg += delta; // accumulate the true rotation delta + lastWrappedDeg = wrapped; // save for the next call } + /** + * Reads the raw encoder voltage, clamps it to [0, maxVoltage], and converts + * it to a wrapped shaft angle in [0, 360°]. + * + * @return current shaft angle in degrees, range [0, 360) + */ private double getWrappedAngle() { - double v = clamp(encoder.getVoltage(), 0.0, maxVoltage); - return (v / maxVoltage) * degreesPerRev; + double v = clamp(encoder.getVoltage(), 0.0, maxVoltage); // clamp ADC reading + return (v / maxVoltage) * degreesPerRev; // linear voltage → degrees } - /* ================= UTIL ================= */ + // ----------------------------------------------------------------------- + // Utility + // ----------------------------------------------------------------------- + + /** Clamps {@code v} to the range [min, max]. */ private double clamp(double v, double min, double max) { return Math.max(min, Math.min(max, v)); } + + /** + * Positive modulo (always returns a value in [0, m)). + * Unlike Java's {@code %} which can return negative values, this helper + * ensures the result is non-negative, matching mathematical modulo behaviour. + */ private double mod(double v, double m) { double r = v % m; - return (r < 0) ? r + m : r; + return (r < 0) ? r + m : r; // shift negative remainder into [0, m) } - /* ================= DEBUG ================= */ + // ----------------------------------------------------------------------- + // Debug / telemetry accessors + // ----------------------------------------------------------------------- + + /** + * Returns the current continuous (unwrapped) shaft angle in degrees. + * Used for telemetry and diagnostic logging. + * + * @return continuously-tracked shaft angle (may exceed 360° or be negative) + */ public double getCurrentAngle() { return continuousDeg; } + + /** + * Returns the current continuous-space target angle in degrees. + * + * @return target angle in continuous space + */ public double getTargetAngle() { return targetDeg; } + + /** + * Returns the target angle converted back to an encoder voltage equivalent. + * Useful for telemetry to verify that the target position maps to a valid + * encoder reading. + * + * @return expected encoder voltage for the target angle (0–maxVoltage) + */ public double getTargetVoltage() { + // Wrap the continuous target back into [0, 360°]. double wrappedDeg = targetDeg % degreesPerRev; - if (wrappedDeg < 0) wrappedDeg += degreesPerRev; + if (wrappedDeg < 0) wrappedDeg += degreesPerRev; // ensure positive result + + // Convert degrees to voltage using the inverse of getWrappedAngle(). return (wrappedDeg / degreesPerRev) * maxVoltage; } + + /** + * Returns the raw encoder voltage from the analog input. + * Used in telemetry to compare against the expected target voltage. + * + * @return live encoder voltage in volts + */ public double getVoltage() { return encoder.getVoltage(); } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/ColorSensorSystem.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/ColorSensorSystem.java index 9031f2a3107c..768db54ba3d2 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/ColorSensorSystem.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/ColorSensorSystem.java @@ -7,151 +7,333 @@ import org.firstinspires.ftc.robotcore.external.Telemetry; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * ColorSensorSystem wraps the robot's normalized colour sensor and provides + * methods to detect whether an artifact (game element) is present and, if so, + * classify its colour as GREEN, PURPLE, EMPTY, or UNKNOWN. + * + *

Classification is performed in HSV (Hue, Saturation, Value) colour space + * rather than raw RGB, because HSV separates the chromatic content (hue) from + * the lighting level (value), making it more robust to ambient light variation. + * + *

Detection pipeline for a single sample: + *

    + *
  1. Read the normalized RGBA values from the sensor.
  2. + *
  3. If {@code alpha < PRESENCE_ALPHA_THRESHOLD} → EMPTY (nothing close enough).
  4. + *
  5. Convert RGB to HSV.
  6. + *
  7. If {@code saturation < MIN_SATURATION} or {@code value < MIN_VALUE} → UNKNOWN + * (artifact is too grey or too dark to determine colour reliably).
  8. + *
  9. If hue falls in the GREEN range → GREEN.
  10. + *
  11. If hue falls in the PURPLE range → PURPLE.
  12. + *
  13. Otherwise → UNKNOWN.
  14. + *
+ * + *

Annotated with {@link Config} so all thresholds and gain values can be + * tuned live from the FTC Dashboard. + */ @Config public class ColorSensorSystem { - // Sensor gain to be tuned + // ----------------------------------------------------------------------- + // Sensor configuration + // ----------------------------------------------------------------------- + + /** Analog gain applied to the sensor's LED/photodiode pair. + * Higher gain increases sensitivity in dim environments but can saturate + * in bright light; value should be calibrated for the competition field. */ public static float SENSOR_GAIN = 20.0f; - // Minimum alpha (light) required to consider the slot full + // ----------------------------------------------------------------------- + // Presence detection threshold + // ----------------------------------------------------------------------- + + /** Minimum normalized alpha (light intensity) required to conclude that + * an artifact is physically close to the sensor. Alpha is a proxy for + * how much light is reflected back; a value ≥ 0.8 indicates the sensor + * is very close to a surface (artifact present). */ public static float PRESENCE_ALPHA_THRESHOLD = 0.8f; - // HSV classification - // Hue is in degrees + // ----------------------------------------------------------------------- + // Green hue range (degrees in HSV) + // ----------------------------------------------------------------------- + + /** Minimum hue angle (°) for the GREEN classification region. */ public static float GREEN_H_MIN = 90f; + + /** Maximum hue angle (°) for the GREEN classification region. */ public static float GREEN_H_MAX = 165f; + // ----------------------------------------------------------------------- + // Purple hue range (degrees in HSV) + // ----------------------------------------------------------------------- + + /** Minimum hue angle (°) for the PURPLE classification region. */ public static float PURPLE_H_MIN = 200f; + + /** Maximum hue angle (°) for the PURPLE classification region. */ public static float PURPLE_H_MAX = 250f; - // Gate out low quality color (gray/too dark) + // ----------------------------------------------------------------------- + // Quality gates (reject low-quality colour reads) + // ----------------------------------------------------------------------- + + /** Minimum saturation required for a colour read to be trusted. + * Very low saturation = grey-ish colour that does not map reliably to + * any specific hue range. */ public static float MIN_SATURATION = 0.25f; + + /** Minimum value (brightness) required for a colour read to be trusted. + * Very dark readings produce unreliable hue values. */ public static float MIN_VALUE = 0.10f; - // hardware + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** The normalized colour sensor hardware object, mapped to the config + * name {@code "color"}. */ private final NormalizedColorSensor color; - // constructor + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Initialises the colour sensor system, retrieves the sensor from the + * hardware map, and sets its analog gain. + * + * @param hardwareMap the robot's hardware map, used to look up the sensor + * registered under the name {@code "color"} + */ public ColorSensorSystem(HardwareMap hardwareMap) { - color = hardwareMap.get(NormalizedColorSensor.class, "color"); - color.setGain(SENSOR_GAIN); + color = hardwareMap.get(NormalizedColorSensor.class, "color"); // fetch sensor by config name + color.setGain(SENSOR_GAIN); // apply the configured sensor gain } - // api + // ----------------------------------------------------------------------- + // Public API + // ----------------------------------------------------------------------- + + /** + * Returns {@code true} if the sensor detects that an artifact is present + * (i.e., {@code alpha >= PRESENCE_ALPHA_THRESHOLD}). + * + *

Alpha is used as a proximity indicator because a highly reflective + * surface at close range returns much more light than an empty slot. + * + * @return {@code true} if an artifact is close to the sensor + */ public boolean hasArtifact() { - NormalizedRGBA rgba = color.getNormalizedColors(); - return rgba.alpha >= PRESENCE_ALPHA_THRESHOLD; + NormalizedRGBA rgba = color.getNormalizedColors(); // single sensor read + return rgba.alpha >= PRESENCE_ALPHA_THRESHOLD; // presence check by reflected light level } + /** + * Classifies the artifact colour (or EMPTY) based on a single sensor sample. + * Returns EMPTY if no artifact is detected (alpha below threshold); + * otherwise delegates to the HSV classifier. + * + * @return classified {@link Indexer.ArtifactColor} + */ public Indexer.ArtifactColor classify() { - NormalizedRGBA rgba = color.getNormalizedColors(); + NormalizedRGBA rgba = color.getNormalizedColors(); // read RGBA from hardware if (rgba.alpha < PRESENCE_ALPHA_THRESHOLD) { - return Indexer.ArtifactColor.EMPTY; + return Indexer.ArtifactColor.EMPTY; // not close enough to an artifact; slot is empty } - return classifyColorOnlyFromRGBA(rgba); + return classifyColorOnlyFromRGBA(rgba); // presence confirmed: classify the colour } + /** + * Classifies the colour assuming an artifact is present. + * Ignores the alpha/presence check and only evaluates the chromatic content. + * Use this when the caller has already confirmed presence by other means. + * + * @return classified {@link Indexer.ArtifactColor} (UNKNOWN if colour is ambiguous) + */ public Indexer.ArtifactColor classifyColorOnly() { - NormalizedRGBA rgba = color.getNormalizedColors(); - return classifyColorOnlyFromRGBA(rgba); + NormalizedRGBA rgba = color.getNormalizedColors(); // read RGBA from hardware + return classifyColorOnlyFromRGBA(rgba); // classify without checking alpha } + /** + * Adds a detailed colour sensor readout to the telemetry display. + * Includes raw RGBA values, converted HSV values, detected artifact colour, + * and the presence flag. Used during tuning sessions. + * + * @param telemetry the FTC telemetry object to write data to + */ public void addTelemetry(Telemetry telemetry) { - NormalizedRGBA rgba = color.getNormalizedColors(); - float[] hsv = rgbToHsv(rgba.red, rgba.green, rgba.blue); + NormalizedRGBA rgba = color.getNormalizedColors(); // read RGBA once + float[] hsv = rgbToHsv(rgba.red, rgba.green, rgba.blue); // convert to HSV for display telemetry.addLine("===== COLOR SENSOR (HSV) ====="); - telemetry.addData("Detected Artifact", classify()); - telemetry.addData("Has Artifact", hasArtifact()); + telemetry.addData("Detected Artifact", classify()); // full classification result + telemetry.addData("Has Artifact", hasArtifact()); // presence flag + // Raw RGBA components (normalized 0–1 by the SDK) telemetry.addData("R", "%.4f", rgba.red); telemetry.addData("G", "%.4f", rgba.green); telemetry.addData("B", "%.4f", rgba.blue); telemetry.addData("Alpha", "%.4f", rgba.alpha); - telemetry.addData("H", "%.1f°", hsv[0]); - telemetry.addData("S", "%.4f", hsv[1]); - telemetry.addData("V", "%.4f", hsv[2]); + // HSV components derived from the RGB reading + telemetry.addData("H", "%.1f°", hsv[0]); // hue in degrees [0, 360) + telemetry.addData("S", "%.4f", hsv[1]); // saturation [0, 1] + telemetry.addData("V", "%.4f", hsv[2]); // value/brightness [0, 1] } + /** + * Returns the raw normalized alpha (light intensity) from the sensor. + * Values close to 1.0 indicate a very nearby surface; values near 0 + * indicate an empty/far slot. + * + * @return normalized alpha in the range [0, 1] + */ public float getAlpha() { - return color.getNormalizedColors().alpha; + return color.getNormalizedColors().alpha; // single read, return alpha only } + /** + * Returns the raw normalized RGB values as a three-element float array. + * + * @return {@code float[]{ red, green, blue }} each in [0, 1] + */ public float[] getRGB() { NormalizedRGBA rgba = color.getNormalizedColors(); return new float[]{ rgba.red, rgba.green, rgba.blue }; } - //its so peak + /** + * Returns the HSV representation of the current sensor reading. + * + * @return {@code float[]{ hue°, saturation, value }} where hue ∈ [0, 360), + * saturation ∈ [0, 1], value ∈ [0, 1] + */ + // its so peak public float[] getHSV() { - NormalizedRGBA rgba = color.getNormalizedColors(); - return rgbToHsv(rgba.red, rgba.green, rgba.blue); + NormalizedRGBA rgba = color.getNormalizedColors(); // read RGB + return rgbToHsv(rgba.red, rgba.green, rgba.blue); // convert and return } - //helprs - + // ----------------------------------------------------------------------- + // Internal classification helpers + // ----------------------------------------------------------------------- + + /** + * Internal method: classifies colour from an already-read RGBA sample. + * Assumes presence has already been confirmed (alpha check done externally). + * + *

Steps: + *

    + *
  1. Convert RGB to HSV.
  2. + *
  3. If saturation or value are below quality thresholds → UNKNOWN.
  4. + *
  5. Check GREEN hue range → GREEN.
  6. + *
  7. Check PURPLE hue range → PURPLE.
  8. + *
  9. Default → UNKNOWN.
  10. + *
+ * + * @param rgba the RGBA sample to classify + * @return classified {@link Indexer.ArtifactColor} + */ private Indexer.ArtifactColor classifyColorOnlyFromRGBA(NormalizedRGBA rgba) { - float[] hsv = rgbToHsv(rgba.red, rgba.green, rgba.blue); - float h = hsv[0]; - float s = hsv[1]; - float v = hsv[2]; + float[] hsv = rgbToHsv(rgba.red, rgba.green, rgba.blue); // convert to HSV + float h = hsv[0]; // hue in degrees + float s = hsv[1]; // saturation [0, 1] + float v = hsv[2]; // value/brightness [0, 1] + // Quality gate: reject achromatic or very dark readings. if (s < MIN_SATURATION || v < MIN_VALUE) { - return Indexer.ArtifactColor.UNKNOWN; + return Indexer.ArtifactColor.UNKNOWN; // colour is too grey or too dark to trust } + // Check whether the hue falls in the green range. if (inHueRange(h, GREEN_H_MIN, GREEN_H_MAX)) { return Indexer.ArtifactColor.GREEN; } + // Check whether the hue falls in the purple range. if (inHueRange(h, PURPLE_H_MIN, PURPLE_H_MAX)) { return Indexer.ArtifactColor.PURPLE; } - return Indexer.ArtifactColor.UNKNOWN; + return Indexer.ArtifactColor.UNKNOWN; // hue did not match any known colour range } - // + /** + * Returns {@code true} if {@code hueDeg} falls within the circular hue + * range [minDeg, maxDeg]. Handles wrap-around ranges that cross 0°/360° + * (e.g., red hues near 0°/360°). + * + * @param hueDeg hue angle in degrees to test + * @param minDeg start of the hue range (inclusive) + * @param maxDeg end of the hue range (inclusive) + * @return {@code true} if the hue is within the specified range + */ private boolean inHueRange(float hueDeg, float minDeg, float maxDeg) { - hueDeg = wrapHue(hueDeg); + hueDeg = wrapHue(hueDeg); // normalize input to [0, 360) minDeg = wrapHue(minDeg); maxDeg = wrapHue(maxDeg); if (minDeg <= maxDeg) { + // Non-wrapping range: simply check if hue is between min and max. return hueDeg >= minDeg && hueDeg <= maxDeg; } else { - // wrap-around (e.g., 300..30) + // Wrap-around range (e.g., 300–30°): hue is in range if ≥ min OR ≤ max. return hueDeg >= minDeg || hueDeg <= maxDeg; } } + /** + * Wraps a hue value into [0, 360). + * + * @param h hue angle in degrees (any value) + * @return equivalent hue in [0, 360) + */ private float wrapHue(float h) { - h %= 360f; - if (h < 0) h += 360f; + h %= 360f; // reduce to (-360, 360) + if (h < 0) h += 360f; // shift negatives into [0, 360) return h; } - //Normalised RGB to HSV conversion + /** + * Converts normalized RGB to HSV colour space. + * + *

The conversion follows the standard algorithm: + *

+ * + * @param r normalized red channel [0, 1] + * @param g normalized green channel [0, 1] + * @param b normalized blue channel [0, 1] + * @return {@code float[]{ H, S, V }} where H ∈ [0, 360), S ∈ [0, 1], V ∈ [0, 1] + */ + // Normalized RGB to HSV conversion private float[] rgbToHsv(float r, float g, float b) { - float max = Math.max(r, Math.max(g, b)); - float min = Math.min(r, Math.min(g, b)); - float delta = max - min; + float max = Math.max(r, Math.max(g, b)); // largest channel = Value + float min = Math.min(r, Math.min(g, b)); // smallest channel + float delta = max - min; // chroma = range of the three channels float h; if (delta < 1e-6f) { + // All channels equal → achromatic grey; hue is undefined, use 0. h = 0f; } else if (max == r) { + // Red is dominant channel: hue in [−60°, 60°) relative to red. h = 60f * (((g - b) / delta) % 6f); } else if (max == g) { + // Green is dominant: hue in [60°, 180°). h = 60f * (((b - r) / delta) + 2f); } else { + // Blue is dominant: hue in [180°, 300°). h = 60f * (((r - g) / delta) + 4f); } - if (h < 0f) h += 360f; + if (h < 0f) h += 360f; // ensure hue is non-negative - float s = (max <= 1e-6f) ? 0f : (delta / max); - float v = max; + float s = (max <= 1e-6f) ? 0f : (delta / max); // saturation; 0 for near-black + float v = max; // value = brightness = max channel return new float[]{ h, s, v }; } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotObservation.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotObservation.java index 479fd018eb3a..c3049bff0198 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotObservation.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotObservation.java @@ -2,51 +2,150 @@ import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * SlotObservation maintains a running tally of colour classifications + * recorded by the colour sensor while the indexer is positioned at a + * particular slot. + * + *

Each call to {@link #record(Indexer.ArtifactColor)} increments the + * counter for the observed colour. After enough observations have accumulated + * ({@code totalHits() >= MIN_HITS_FOR_DECISION} in the Indexer), the slot's + * colour can be resolved with confidence via + * {@link #resolveWithThreshold(double, double, double, double)}. + * + *

The {@link #trimToMax(int)} method scales all counters proportionally + * when the total exceeds a cap, keeping the computation lightweight and + * preventing old data from dominating indefinitely (sliding-window effect). + */ public class SlotObservation { + + // ----------------------------------------------------------------------- + // Hit counters (one per colour category) + // ----------------------------------------------------------------------- + + /** Number of times a GREEN artifact was detected while observing this slot. */ public int greenHits = 0; + + /** Number of times a PURPLE artifact was detected while observing this slot. */ public int purpleHits = 0; + + /** Number of times the slot appeared EMPTY while being observed. */ public int emptyHits = 0; + + /** Number of times the colour sensor returned UNKNOWN (artifact present but + * ambiguous hue – often an artifact in transit or a bad sensor read). */ public int unknownHits = 0; - public void reset() { greenHits = purpleHits = emptyHits = unknownHits = 0; } + // ----------------------------------------------------------------------- + // Reset + // ----------------------------------------------------------------------- + + /** + * Resets all hit counters to zero. + * Called when a slot's stored colour is re-initialised or when the indexer + * moves away from the slot and fresh observations need to start clean. + */ + public void reset() { + greenHits = purpleHits = emptyHits = unknownHits = 0; // zero every counter in one assignment chain + } + // ----------------------------------------------------------------------- + // Recording + // ----------------------------------------------------------------------- + + /** + * Records a single colour observation for this slot. + * Increments the counter that corresponds to the supplied colour. + * + * @param c the colour classification from the colour sensor for this cycle + */ public void record(Indexer.ArtifactColor c) { switch (c) { case GREEN: - greenHits++; + greenHits++; // saw a green artifact; accumulate green evidence break; case PURPLE: - purpleHits++; + purpleHits++; // saw a purple artifact; accumulate purple evidence break; case EMPTY: - emptyHits++; + emptyHits++; // slot appeared empty; accumulate empty evidence break; case UNKNOWN: - unknownHits++; + unknownHits++; // ambiguous read; accumulate unknown evidence break; } } - public int totalHits() { return greenHits + purpleHits + emptyHits + unknownHits; } + // ----------------------------------------------------------------------- + // Aggregation helpers + // ----------------------------------------------------------------------- + + /** + * Returns the total number of observations recorded so far. + * Used to check whether enough data has been gathered before making + * a colour decision. + * + * @return sum of all four hit counters + */ + public int totalHits() { + return greenHits + purpleHits + emptyHits + unknownHits; + } + /** + * Scales down all counters proportionally so that the total does not + * exceed {@code maxTotal}. This implements a simple sliding-window + * effect: very old observations are gradually aged out while still + * influencing the overall ratio. + * + *

If the current total is already within the limit this method is a no-op. + * + * @param maxTotal the maximum number of total observations to retain + */ public void trimToMax(int maxTotal) { int total = totalHits(); - if (total <= maxTotal) return; + if (total <= maxTotal) return; // already within the cap; nothing to do + + // Compute the scale factor needed to bring the total down to maxTotal. double scale = maxTotal / (double) total; - greenHits = (int) Math.round(greenHits * scale); + + // Round each counter to the nearest integer after scaling. + greenHits = (int) Math.round(greenHits * scale); purpleHits = (int) Math.round(purpleHits * scale); - emptyHits = (int) Math.round(emptyHits * scale); - unknownHits = (int) Math.round(unknownHits * scale); + emptyHits = (int) Math.round(emptyHits * scale); + unknownHits= (int) Math.round(unknownHits* scale); } + // ----------------------------------------------------------------------- + // Colour decision + // ----------------------------------------------------------------------- + + /** + * Resolves the most likely slot colour from the accumulated observations + * using per-colour fraction thresholds. + * + *

For each colour, if {@code hits / totalHits >= threshold} the method + * returns that colour immediately (first match wins, so ordering matters: + * GREEN is checked first, then PURPLE, EMPTY, UNKNOWN). If no threshold + * is exceeded the method falls back to {@link Indexer.ArtifactColor#UNKNOWN}. + * + *

Returns {@link Indexer.ArtifactColor#EMPTY} when no observations exist + * yet (to default to "slot is free"). + * + * @param greenThresh fraction of total hits that must be GREEN to declare GREEN + * @param purpleThresh fraction of total hits that must be PURPLE to declare PURPLE + * @param emptyThresh fraction of total hits that must be EMPTY to declare EMPTY + * @param unknownThresh fraction of total hits that must be UNKNOWN to declare UNKNOWN + * @return the resolved {@link Indexer.ArtifactColor} + */ public Indexer.ArtifactColor resolveWithThreshold(double greenThresh, double purpleThresh, double emptyThresh, double unknownThresh) { int total = totalHits(); - if (total == 0) return Indexer.ArtifactColor.EMPTY; + if (total == 0) return Indexer.ArtifactColor.EMPTY; // no data yet; treat as empty by default - if (greenHits / (double) total >= greenThresh) return Indexer.ArtifactColor.GREEN; - if (purpleHits / (double) total >= purpleThresh) return Indexer.ArtifactColor.PURPLE; - if (emptyHits / (double) total >= emptyThresh) return Indexer.ArtifactColor.EMPTY; - if (unknownHits / (double) total >= unknownThresh) return Indexer.ArtifactColor.UNKNOWN; + // Compare each colour's fraction to its threshold (first match wins). + if (greenHits / (double) total >= greenThresh) return Indexer.ArtifactColor.GREEN; + if (purpleHits / (double) total >= purpleThresh) return Indexer.ArtifactColor.PURPLE; + if (emptyHits / (double) total >= emptyThresh) return Indexer.ArtifactColor.EMPTY; + if (unknownHits/ (double) total >= unknownThresh) return Indexer.ArtifactColor.UNKNOWN; return Indexer.ArtifactColor.UNKNOWN; // fallback if nothing crosses threshold } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotState.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotState.java index 848eb5f465c8..371e0a6bb874 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotState.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/indexerUtil/SlotState.java @@ -2,10 +2,61 @@ import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * SlotState bundles all mutable per-slot data for one indexer position. + * + *

The indexer drum has three equally-spaced slots (0, 1, 2). Each slot + * independently tracks: + *

+ * + *

An instance of this class is held in the {@code slots[]} array inside + * {@link Indexer}, one entry per physical slot. + */ public class SlotState { + + // ----------------------------------------------------------------------- + // Fields + // ----------------------------------------------------------------------- + + /** The resolved (or manually assigned) colour currently stored in this slot. + * Defaults to UNKNOWN so the indexer knows it has not yet been classified. */ public Indexer.ArtifactColor color = Indexer.ArtifactColor.UNKNOWN; + + /** Accumulated sensor hit counts for this slot. + * Used to probabilistically resolve the slot's colour from multiple + * sequential sensor reads rather than from a single noisy sample. */ public SlotObservation obs = new SlotObservation(); + + /** {@code true} when the slot was EMPTY the last time the colour was + * evaluated. Transitions from {@code true} → {@code false} indicate that + * a new artifact has entered this slot, triggering fill-cycle logic. */ public boolean wasEmpty = true; - public int fillingHits = 0; // counts sensorNonEmpty hits during a fill cycle - public boolean fillCycleActive = false; // latched after stored EMPTY + sensor nonempty + + /** Running count of consecutive sensor-detected "non-empty" readings during + * the current fill cycle. Once this reaches the required threshold the + * auto-advance logic accepts that the artifact is fully seated and can + * move the indexer to the next slot. */ + public int fillingHits = 0; + + /** {@code true} once the slot has been confirmed to be transitioning from + * EMPTY to a filled state (artifact detected after stored EMPTY). + * Reset to {@code false} when the fill cycle completes or the slot is + * re-initialised. */ + public boolean fillCycleActive = false; } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/Aimer.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/Aimer.java index 36bf31fb50c9..b2dae95f5ec3 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/Aimer.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/Aimer.java @@ -7,148 +7,361 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; +/** + * Aimer computes the heading-correction power needed to point the robot at + * a scoring goal using the Road Runner localizer instead of live camera data. + * + *

Field coordinate convention used by this class: + *

+ * + *

Workflow: + *

    + *
  1. Call {@link #setRedTarget()} or {@link #setBlueTarget()} to aim at + * the alliance-specific goal.
  2. + *
  3. Call {@link #relocalize()} when the robot is at the human player zone + * to re-zero the Road Runner localizer at a known field position.
  4. + *
  5. Each loop iteration, call {@link #calculateLocalizedTurnPower()} to + * get a {@code double[3]} array: {@code {turnPower, range, bearingDeg}}.
  6. + *
  7. Feed {@code turnPower} into {@link org.firstinspires.ftc.teamcode.subsystems.Movement} + * as the {@code turnCorrection} parameter.
  8. + *
+ * + *

A PIDF controller is used for the heading loop; all gains and the target + * pose can be tuned live in FTC Dashboard because the class is annotated with + * {@link Config}. + */ @Config public class Aimer { - //bottom left of field is (0,0) (red human player), top right (near red goal) is (144,144) - //bottom right of field is (144,0) (blue human player), top left (near blue goal) is (0,144) - //the bot is relocalized at the respective human player zone + // ----------------------------------------------------------------------- + // PIDF gains and filter + // ----------------------------------------------------------------------- + + /** Proportional gain for the heading PID loop. + * Higher values produce faster corrections but risk oscillation. */ public static double kP = 0.06; + + /** Integral gain for the heading PID loop. Accumulates bearing error over + * time to eliminate steady-state offset. Currently 0. */ public static double kI = 0.0; + + /** Derivative gain for the heading PID loop. Damps rapid bearing changes. + * Currently 0; a low-pass filter on the derivative is also applied. */ public static double kD = 0.0; + + /** Feedforward gain: a small constant power applied in the direction of the + * bearing error to overcome static friction in the drive motors. */ public static double kF = 0.0; - public static double filter = 0.867; // smoothing factor (1 = no filtering, 0 = very heavy smoothing) + + /** Derivative smoothing coefficient (0–1). + * 1.0 = no smoothing (raw derivative); 0.0 = maximum smoothing (holds last value). + * The exponential moving average is: {@code d = (1 - filter)*prevD + filter*rawD}. */ + public static double filter = 0.867; + + /** Maximum absolute integral value (anti-windup clamp). + * Prevents the integral from accumulating large values when the robot is far off-aim. */ public static double maxIntegral = 1.0; - public static double deadband = 1; + + /** Bearing deadband (degrees). When |error| < deadband no correction is applied + * and the integral is reset to prevent chattering when the robot is aimed well. */ + public static double deadband = 1; // degrees + + // ----------------------------------------------------------------------- + // PID internal state + // ----------------------------------------------------------------------- + + /** Accumulated bearing error integral (degrees × seconds). */ private double integral = 0; + + /** Smoothed derivative of the bearing error from the last call. */ private double lastDerivative = 0.0; + + /** Bearing error on the previous iteration (degrees), used to compute the derivative. */ private double lastError = 0; + + /** System time (ms) of the last {@link #calculateTurnPowerFromBearing(double)} call. + * Used to compute dt for the I and D terms. */ private long lastTimestamp = 0; + + // ----------------------------------------------------------------------- + // Dependencies + // ----------------------------------------------------------------------- + + /** Road Runner mecanum drive; provides the localizer pose estimate. */ private final MecanumDrive drive; - //private final InertiaAutoAim inertiaAutoAim; + + // ----------------------------------------------------------------------- + // Target configuration + // ----------------------------------------------------------------------- + + /** Field-relative pose of the target AprilTag (scoring goal). + * Updated by {@link #setRedTarget()} or {@link #setBlueTarget()}. + * Default heading of 90° means the tag faces the field's +Y direction. */ public static Pose2d tagPose = new Pose2d(0, 132, Math.toRadians(90)); + + /** Height of the robot's camera/sensor above the floor (inches). */ public static double cameraHeight = 11.815; // inches + + /** Height of the goal AprilTag above the floor (inches). + * Used to compute the 3-D range from the 2-D horizontal distance. */ public static double goalAprilTagHeight = 29.5; // inches - public static double goalBack = 4; //how far from the back of the field the aiming point is - public static double goalOut = 15; //how far from the side border of the field (where drivers stand) the aiming point is + /** Horizontal distance (inches) from the back field wall to the aiming point. + * Adjusts where on the field the robot tries to aim "at" the goal from. */ + public static double goalBack = 4; // inches from back wall of the field + /** Lateral distance (inches) from the side border (driver station wall) to + * the aiming point. Positions the goal target away from the wall. */ + public static double goalOut = 15; // inches from driver side wall + + // ----------------------------------------------------------------------- + // Goal selection + // ----------------------------------------------------------------------- + + /** + * Enum representing the two alliance goals on the field. + */ public enum Goal { + /** Red alliance scoring goal (top-right area of the field). */ RED, + /** Blue alliance scoring goal (top-left area of the field). */ BLUE } + /** The currently selected alliance goal. Written by {@link #setRedTarget()} + * and {@link #setBlueTarget()}; read by {@link #relocalize()}. */ public static Goal selectedGoal = Goal.RED; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an Aimer and immediately configures it for the red goal. + * Also re-zeros the localizer pose to the robot's assumed starting position + * at the human player zone. + * + * @param mecanumDrive the Road Runner drive used to read the localizer pose + */ public Aimer(MecanumDrive mecanumDrive) { this.drive = mecanumDrive; - //defaults to red goal + // Default to the red goal on construction; can be changed before a match. setRedTarget(); + // Re-zero the localizer at the human player starting position. relocalize(); - //inertiaAutoAim = new InertiaAutoAim(); } + // ----------------------------------------------------------------------- + // Goal selection helpers + // ----------------------------------------------------------------------- + + /** + * Configures the aimer to target the red alliance goal (top-right + * corner, approximately (144−goalOut, 144−goalBack) on the field). + * Also switches to the red AprilTag pipeline implicitly via the caller. + */ public void setRedTarget(){ selectedGoal = Goal.RED; - tagPose = new Pose2d(144-goalOut,144-goalBack, Math.toRadians(90)); + // Compute the red goal's field position from the back/side offsets. + tagPose = new Pose2d(144-goalOut, 144-goalBack, Math.toRadians(90)); } + /** + * Configures the aimer to target the blue alliance goal (top-left + * corner, approximately (goalOut, 144−goalBack) on the field). + */ public void setBlueTarget(){ selectedGoal = Goal.BLUE; - tagPose = new Pose2d(goalOut,144-goalBack, Math.toRadians(90)); + // Compute the blue goal's field position from the back/side offsets. + tagPose = new Pose2d(goalOut, 144-goalBack, Math.toRadians(90)); } + /** + * Returns the currently selected alliance goal. + * + * @return {@link Goal#RED} or {@link Goal#BLUE} + */ public Goal getGoal() { return selectedGoal; } + // ----------------------------------------------------------------------- + // Localization + // ----------------------------------------------------------------------- + + /** + * Re-zeros the Road Runner localizer at the robot's known starting position + * in the human player zone. + * + *

The robot is assumed to be flush against the human player wall, + * oriented at 90° (facing the field). A {@code botThick} offset accounts + * for the robot's physical half-width so that the localizer's (x,y) refers + * to the robot center, not the field boundary. + */ public void relocalize(){ + // Half-width of the robot in inches; used to place the center away from the wall. int botThick = 9; - //RED Human player zone if(selectedGoal == Goal.RED){ + // Red human player zone: bottom-left corner, heading 90° (facing +Y). drive.localizer.setPose(new Pose2d(0+botThick, 0+botThick, Math.toRadians(90))); } else if (selectedGoal == Goal.BLUE){ + // Blue human player zone: bottom-right corner, heading 90° (facing +Y). drive.localizer.setPose(new Pose2d(144-botThick, 0+botThick, Math.toRadians(90))); } } + // ----------------------------------------------------------------------- + // Main aiming computation + // ----------------------------------------------------------------------- + + /** + * Computes the turn correction power, range, and bearing to the goal using + * the current localizer pose estimate. + * + *

Steps: + *

    + *
  1. Read the robot's current field pose from the Road Runner localizer.
  2. + *
  3. Compute the 2-D horizontal vector from robot to the goal tag pose.
  4. + *
  5. Combine with the camera–goal height difference to get the 3-D range.
  6. + *
  7. Compute the desired heading (atan2 of the goal vector).
  8. + *
  9. Subtract the robot's current heading to get the bearing error.
  10. + *
  11. Run the PID controller on the bearing to produce a turn power.
  12. + *
+ * + * @return {@code double[]{turnPower, range, bearing}} where: + * + */ public double[] calculateLocalizedTurnPower() { + // Get the robot's current estimated field pose from the Road Runner localizer. Pose2d robotPose = drive.localizer.getPose(); - // Vector from robot -> tag in field coordinates + // 2-D vector from robot to goal tag (field coordinates, inches). double dx = tagPose.position.x - robotPose.position.x; double dy = tagPose.position.y - robotPose.position.y; + // Horizontal (ground-plane) distance from robot to goal. double horizontalDistance = Math.hypot(dx, dy); + // Height difference between the goal tag and the camera mounting height. double dz = goalAprilTagHeight - cameraHeight; - // point-to-point distance= + // 3-D straight-line distance: combines horizontal and vertical separation. double range = Math.hypot(horizontalDistance, dz); + // Desired robot heading to face the goal (radians, field frame). double desiredHeading = Math.atan2(dy, dx); + // Current robot heading from the localizer (radians, field frame). double currentHeading = robotPose.heading.toDouble(); + // Bearing = angular difference between desired and current heading (degrees). double bearing = Math.toDegrees(desiredHeading - currentHeading); - bearing = angleWrapDegrees(bearing); + bearing = angleWrapDegrees(bearing); // wrap to [-180, 180] + // Compute the PID turn power (negated so positive bearing → positive turn output). double turnPower = -calculateTurnPowerFromBearing(bearing); return new double[]{turnPower, range, bearing}; } + // ----------------------------------------------------------------------- + // Angle helpers + // ----------------------------------------------------------------------- + + /** + * Wraps a degree angle into the range (-180, 180]. + * + *

Used to ensure heading errors are always expressed as the shortest + * angular path (avoiding ±360 jumps). + * + * @param angle raw angle in degrees + * @return equivalent angle in (-180, 180] + */ private double angleWrapDegrees(double angle) { + // Shift by 180, take mod 360, then shift back so result is in (-180, 180]. return (angle + 180) % 360 - 180; } + // ----------------------------------------------------------------------- + // PID controller + // ----------------------------------------------------------------------- + + /** + * Runs the PIDF heading controller and returns a motor power in [-1, 1]. + * + *

If the bearing is {@link Double#NaN} (e.g., tag not visible) all PID + * state is reset and 0 is returned. If the bearing is within the + * {@link #deadband} the integral is reset and 0 is returned. + * + * @param bearing current heading error in degrees; + * {@link Double#NaN} if aiming data is unavailable + * @return turn correction power in [-1.0, 1.0]; positive turns right + */ public double calculateTurnPowerFromBearing(double bearing) { - // If apriltag lost - reset PID state and return no correction + // If bearing is NaN (e.g., tag lost), reset all PID state and return no correction. if (Double.isNaN(bearing)) { - integral = 0; - lastError = 0; - lastDerivative = 0; - lastTimestamp = 0; - return 0.0; + integral = 0; // clear integral accumulation + lastError = 0; // clear previous error + lastDerivative = 0; // clear smoothed derivative + lastTimestamp = 0; // force dt to be treated as first call on resume + return 0.0; // no correction while tag is not visible } + // Wrap bearing to (-180, 180] to ensure shortest-path error. double error = angleWrapDegrees(bearing); + if (Math.abs(error) < deadband) { + // Within the deadband: on-target enough; reset integral and return 0. integral = 0; - lastError = error; - lastDerivative = 0; - return 0; + lastError = error; // keep last error so derivative is smooth on next entry + lastDerivative = 0; // reset derivative since we're stationary in the band + return 0; // no correction needed } - // Time diff in seconds + // ---- Compute dt ---- long currentTime = System.currentTimeMillis(); double deltaTime; - if (lastTimestamp == 0) { - deltaTime = 0.01; // assume 10 ms on first loop + deltaTime = 0.01; // assume 10 ms on the very first call (no previous timestamp) } else { - deltaTime = (currentTime - lastTimestamp) / 1000.0; + deltaTime = (currentTime - lastTimestamp) / 1000.0; // convert ms to seconds } - lastTimestamp = currentTime; + lastTimestamp = currentTime; // store for next iteration - // Don't consider > 100 ms or < 5ms - deltaTime = Math.max(Math.min(deltaTime, 0.1), .005); + // Clamp dt to [5 ms, 100 ms] to avoid numerical issues from very short or + // very long gaps between control loop iterations. + deltaTime = Math.max(Math.min(deltaTime, 0.1), 0.005); - // Integral accumulation - integral += error * deltaTime; + // ---- Integral ---- + integral += error * deltaTime; // accumulate error × time + // Anti-windup: clamp so the integral can't grow unboundedly. integral = Math.max(-maxIntegral, Math.min(maxIntegral, integral)); - // Derivative with smoothing - double rawDerivative = (error - lastError) / deltaTime; + // ---- Derivative with low-pass filter ---- + double rawDerivative = (error - lastError) / deltaTime; // rate of change of error + // Exponential moving average: blends previous smoothed derivative with raw one. double derivative = (1 - filter) * lastDerivative + filter * rawDerivative; - lastDerivative = derivative; - lastError = error; + lastDerivative = derivative; // save for next iteration + lastError = error; // save current error for next iteration - // Gets proper direction + // ---- Feedforward ---- + // A small constant in the direction of the error to overcome stiction. double feedforward = Math.signum(error) * kF; - // output + // ---- Combined PIDF output ---- double power = kP * error + kI * integral + kD * derivative + feedforward; + // Clamp output to valid motor power range. return Math.max(-1, Math.min(1, power)); } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/AprilTag.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/AprilTag.java index 3b0124cfc29c..9bcc849396e4 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/AprilTag.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/AprilTag.java @@ -8,97 +8,312 @@ import java.util.List; +/** + * AprilTag wraps the Limelight 3A vision co-processor to detect and measure + * fiducial AprilTag markers on the FTC field. + * + *

Two types of AprilTag scans are supported: + *

+ * + *

Range is computed from the tag's elevation angle using a geometric formula + * based on the camera height and the known tag height above the floor. + * + *

Alliance-specific notes: + *

+ */ // TODO IMPORTANT NOTES: For goalTagID, just have separate teleops one for red alliance one for blue where blue teleop can setGoalTagID(20) and red teleop can setGoalTagID(24) // TODO We will see whether we want separate auto for either alliance, probably yes its just easier that way and there may be some functionality requiring that. public class AprilTag { + + // ----------------------------------------------------------------------- + // Scan results (updated by scan methods) + // ----------------------------------------------------------------------- + + /** ID of the last tag seen by {@link #scanGoalTag()}. + * {@code -1} if no goal tag has been detected yet. */ private int id = -1; + + /** ID of the last obelisk tag seen by {@link #scanObeliskTag()}. + * Shared across all instances (static) so it can be read globally. + * {@code -1} if no obelisk tag has been detected. */ public static int obeliskId = -1; + + /** ID of the target alliance goal tag. Set externally before scanning + * so the scan methods can filter for the correct tag. */ private int goalTagID; // our current alliance goal + + /** The most recently detected goal tag ID. May differ from {@link #goalTagID} + * if the camera sees a tag other than the intended goal. */ private int cameraScannedId; + + /** Horizontal bearing to the goal tag in degrees (positive = tag is to the + * right of the camera center; negative = to the left). */ private double bearing; + + /** Vertical elevation angle of the goal tag in degrees above the camera + * horizontal (positive = tag is above camera center). */ private double elevation; + + /** Straight-line distance from the camera to the goal tag in inches, + * computed from the elevation angle and known heights. */ private double range; + + /** Pixel area fraction of the goal tag in the camera frame. + * Larger = closer; used as a rough confidence metric. */ private double tagSize; + + // ----------------------------------------------------------------------- + // Hardware + // ----------------------------------------------------------------------- + + /** The Limelight 3A co-processor hardware object, mapped to config name + * {@code "limelight"}. */ private final Limelight3A limelight; + + /** FTC telemetry reference (currently unused but reserved for debug output). */ private final Telemetry telemetry; - private final double LIMELIGHT_HEIGHT = 11.815; - private final double LIMELIGHT_ANGLE = 15; - private final double TARGET_HEIGHT = 29.5; + // ----------------------------------------------------------------------- + // Camera / target geometry constants + // ----------------------------------------------------------------------- + + /** Height of the Limelight camera above the floor in inches. */ + private final double LIMELIGHT_HEIGHT = 11.815; // inches + + /** Upward tilt angle of the Limelight camera from horizontal in degrees. + * Added to the tag's reported elevation when computing the range. */ + private final double LIMELIGHT_ANGLE = 15; // degrees above horizontal + + /** Height of the goal AprilTag above the floor in inches. */ + private final double TARGET_HEIGHT = 29.5; // inches + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an AprilTag instance, retrieves the Limelight from the hardware + * map, switches to pipeline 0 (blue goal), and starts the vision pipeline. + * + * @param hardwareMap the robot's hardware map used to find the Limelight + * device registered under the config name {@code "limelight"} + * @param telemetry the FTC telemetry object (reserved for future debug output) + */ public AprilTag(HardwareMap hardwareMap, Telemetry telemetry) { this.telemetry = telemetry; + // Retrieve the Limelight co-processor from the hardware map. limelight = hardwareMap.get(Limelight3A.class, "limelight"); - limelight.pipelineSwitch(0); - limelight.start(); + limelight.pipelineSwitch(0); // default to pipeline 0 (blue alliance goal) + limelight.start(); // begin streaming and processing frames } + // ----------------------------------------------------------------------- + // Limelight power control + // ----------------------------------------------------------------------- + + /** + * Starts or stops the Limelight pipeline. + * Stopping it when not in use conserves battery and reduces CPU load. + * + * @param bool {@code true} to start the pipeline; {@code false} to stop it + */ public void toggle(boolean bool) { - if (bool) { limelight.start(); } - else { limelight.stop(); } + if (bool) { limelight.start(); } // resume vision processing + else { limelight.stop(); } // pause vision processing } + // ----------------------------------------------------------------------- + // Scan methods + // ----------------------------------------------------------------------- + + /** + * Scans for the obelisk AprilTag (pipeline 2) and stores the detected + * tag ID in {@link #obeliskId}. + * + *

The obelisk tag ID encodes the scoring motif pattern for the current + * round (IDs 21, 22, or 23). Reads the first fiducial in the result list + * and returns immediately; only one obelisk tag is expected to be visible. + */ public void scanObeliskTag() { - setPipeline(2); + setPipeline(2); // switch to pipeline 2 which is configured for obelisk tags + // Get all fiducial detections from the latest frame. List scanned = limelight .getLatestResult() .getFiducialResults(); + // Store the first detected tag ID; only one obelisk tag is expected. for (LLResultTypes.FiducialResult detection : scanned) { - int fid = detection.getFiducialId(); - obeliskId = fid; - return; + int fid = detection.getFiducialId(); // read the numeric tag ID + obeliskId = fid; // cache globally for other classes to read + return; // only need one detection; stop after the first } } + // ----------------------------------------------------------------------- + // Internal geometry helpers + // ----------------------------------------------------------------------- + + /** + * Computes the slant range (inches) from the camera to the tag using the + * known vertical geometry. + * + *

Formula: + *

+     *   range = (TARGET_HEIGHT - LIMELIGHT_HEIGHT) / sin(elevation + LIMELIGHT_ANGLE)
+     * 
+ * where all angles are in degrees. + * + * @param elevation vertical angle of the tag above camera centre (degrees) + * @return slant distance from camera to tag in inches + */ private double calculateDistance(double elevation) { + // sin() of the total angle from horizontal to the tag gives the vertical component; + // dividing the known height difference yields the slant distance. return (TARGET_HEIGHT - LIMELIGHT_HEIGHT) / Math.sin(Math.toRadians(elevation + LIMELIGHT_ANGLE)); } + // ----------------------------------------------------------------------- + // Goal tag scan + // ----------------------------------------------------------------------- + + /** + * Scans for the alliance goal AprilTag and updates {@link #bearing}, + * {@link #elevation}, {@link #range}, and {@link #tagSize}. + * + *

If no tag is found in the current frame, {@link #bearing}, + * {@link #elevation}, and {@link #range} are set to {@link Double#NaN} + * so callers can detect a "tag lost" condition. {@link #id} is reset to + * {@code -1}. + * + *

If multiple tags are visible the last one in the list "wins" (all are + * iterated); in practice only one goal tag should be in frame. + */ public void scanGoalTag() { - id = -1; - /* So that if you scan and theres no tag range stays, (bearing should be reset in the loops)*/ - bearing = Double.NaN; - elevation = Double.NaN; - range = Double.NaN; + id = -1; // reset to no-detection state + /* So that if you scan and there's no tag, range stays NaN (bearing should be reset in loops) */ + bearing = Double.NaN; // no valid bearing until a tag is found + elevation = Double.NaN; // no valid elevation until a tag is found + range = Double.NaN; // no valid range until a tag is found - // If camera is facing to the right of the center of the cam (if it needs to move to the left) the bearing is positive. + // Get all fiducial detections from the latest frame. + // If camera is facing to the right of center the bearing is positive. List scanned = limelight.getLatestResult().getFiducialResults(); for (LLResultTypes.FiducialResult detection: scanned) { - cameraScannedId = detection.getFiducialId(); - // goalTagID should be gotten before round/during auto - id = cameraScannedId; - elevation = detection.getTargetYDegrees(); - range = calculateDistance(elevation); - bearing = detection.getTargetXDegrees(); - tagSize = detection.getTargetArea(); + cameraScannedId = detection.getFiducialId(); // record which tag was seen + // goalTagID should be obtained before the round / during auto + id = cameraScannedId; // store as "current" goal tag ID + elevation = detection.getTargetYDegrees(); // vertical angle from camera centre (degrees) + range = calculateDistance(elevation); // compute slant range from elevation + bearing = detection.getTargetXDegrees(); // horizontal angle from camera centre (degrees) + tagSize = detection.getTargetArea(); // fractional pixel area of the tag } } + // ----------------------------------------------------------------------- + // Configuration + // ----------------------------------------------------------------------- + + /** + * Switches the active Limelight pipeline. + * + *

Pipeline assignments: + *

+ * + * @param pipeline pipeline index to activate (0, 1, or 2) + */ public void setPipeline(int pipeline) { // 0 blue, 1 red, 2 obelisk - limelight.pipelineSwitch(pipeline); + limelight.pipelineSwitch(pipeline); // send the pipeline switch command to the co-processor } + + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + + /** + * Returns the ID of the most recently detected goal tag. + * + * @return AprilTag ID, or 0 if no tag has been scanned yet + */ public int getCurrentId() { - return cameraScannedId; + return cameraScannedId; // last goal tag ID seen by the camera } + + /** + * Returns the ID of the most recently detected obelisk tag. + * This is a static field shared across all instances. + * + * @return obelisk AprilTag ID (21, 22, or 23 for valid motif tags), + * or {@code -1} if none detected + */ public int getObeliskId() { - return obeliskId; + return obeliskId; // shared static field; same value regardless of which instance reads it } + + /** + * Returns the vertical elevation of the goal tag in degrees above the + * camera's horizontal axis. + * + * @return elevation angle in degrees; {@link Double#NaN} if no tag detected + */ public double getElevation() { - return elevation; + return elevation; // degrees above camera centre; used to compute range } + + /** + * Returns the computed slant range to the goal tag in inches. + * + * @return range in inches; {@link Double#NaN} if no tag detected + */ public double getRange() { - return range; + return range; // slant distance from camera to goal tag (inches) } + + /** + * Returns the horizontal bearing to the goal tag in degrees. + * Positive = tag is to the right of the camera; negative = to the left. + * + * @return bearing in degrees; {@link Double#NaN} if no tag detected + */ public double getBearing() { - return bearing; + return bearing; // horizontal angle from camera centre to goal tag (degrees) } + + /** + * Returns the fractional pixel area of the last detected goal tag. + * A larger value indicates the robot is closer to the tag. + * + * @return tag area fraction (0.0–100.0); 0 if no tag detected + */ public double getArea() { - return tagSize; + return tagSize; // tag pixel area as a fraction of the frame; proxy for proximity } + + /** + * Manually overrides the cached camera-scanned goal tag ID. + * Useful during testing or when the obelisk tag ID is known in advance. + * + * @param i the tag ID to store as the current camera-scanned ID + */ public void setCurrentCameraScannedId(int i) { - cameraScannedId = i; + cameraScannedId = i; // override the last detected goal tag ID } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/InertiaAutoAim.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/InertiaAutoAim.java index d7f6b11467d9..c53f35c6afd6 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/InertiaAutoAim.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/limelight/InertiaAutoAim.java @@ -1,26 +1,108 @@ package org.firstinspires.ftc.teamcode.subsystems.limelight; +/** + * InertiaAutoAim computes the yaw correction needed to hit a stationary goal + * when the robot itself is moving. + * + *

Problem: the robot is driving while it fires. A ball fired straight ahead + * will be displaced sideways by the robot's lateral velocity during the ball's + * flight time. To compensate, the aimer should lead the target by the amount + * the ball will drift. + * + *

Algorithm (simplified): + *

    + *
  1. The robot (treated as the coordinate origin) is moving with velocity + * {@code robotVel} (3-D, in robot-frame coordinates).
  2. + *
  3. The goal is at position {@code goalPos} in the robot frame, computed + * from the given range and heading.
  4. + *
  5. During the ball's flight time {@code t = goalDistance / ballSpeed}, the + * robot (and therefore the "effective launch origin") moves by + * {@code robotVel * t}.
  6. + *
  7. The required aim point is the goal position minus that drift; + * {@link Math#atan2} on the resulting vector gives the corrected yaw.
  8. + *
+ * + *

Note: This class is currently not used in the main codebase + * (the reference in {@link Aimer} is commented out) but is available for + * future inertia-compensation work. + */ public class InertiaAutoAim { - //Given: Magnitude and direction of Velocity, and range and elevation - //Get: needed yaw change - private final static double[] ROBOT_POS = {0,0,0}; - // double[] --> {x,y,z} (y is vertical) - public InertiaAutoAim() { + // ----------------------------------------------------------------------- + // Constants + // ----------------------------------------------------------------------- + + /** + * Robot position in the local reference frame – always (0, 0, 0) because + * all calculations are robot-relative. The layout is {x, y, z} where y + * is the vertical axis. + */ + private final static double[] ROBOT_POS = {0, 0, 0}; + // double[] --> {x, y, z} (y is vertical) + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an InertiaAutoAim instance. No parameters are needed because + * all state is passed per-call via {@link #getYawDegrees}. + */ + public InertiaAutoAim() { + // No member state to initialise; all inputs are provided per-call. } + // ----------------------------------------------------------------------- + // Correction computation + // ----------------------------------------------------------------------- + + /** + * Computes the yaw angle correction (in degrees) needed to compensate for + * the robot's translational velocity while firing. + * + *

Coordinate convention: the robot is at (0, 0, 0). X points to the + * robot's right, Y points upward, and Z points forward (in the direction + * the robot is heading). + * + * @param robotVel robot velocity vector as {@code {vx, vy, vz}} in + * inches per second (robot frame; y is vertical) + * @param ballSpeed muzzle speed of the ball in inches per second + * @param robotYawRad current robot heading in radians (field frame); + * used to rotate the goal position into robot frame + * @param goalDistance straight-line distance from robot to goal in inches + * @param goalElevation height of the goal above the robot in inches (vertical component) + * @return yaw correction in degrees; positive = need to aim further left + * (counter-clockwise) to compensate for rightward robot motion + */ public double getYawDegrees(double[] robotVel, double ballSpeed, double robotYawRad, double goalDistance, double goalElevation) { + // Compute the horizontal (ground-plane) distance from robot to goal, + // stripping the vertical elevation component out of the total slant range. double baseDistance = Math.sqrt(goalDistance * goalDistance - goalElevation * goalElevation); - double[] goalPos = {baseDistance * Math.sin(robotYawRad),goalElevation,baseDistance * Math.cos(robotYawRad)}; - // goal pos is relative to the robot pos - double time = (goalDistance / ballSpeed); - // get the needed yaw change - double dx = goalPos[0] - (ROBOT_POS[0] + robotVel[0] * time); - double dz = goalPos[2] - (ROBOT_POS[2] + robotVel[2] * time); + // Represent the goal's position in the robot's local 3-D frame. + // X = right of robot = baseDistance * sin(heading) (lateral field component) + // Y = elevation = goalElevation (vertical) + // Z = forward of robot = baseDistance * cos(heading) (forward field component) + double[] goalPos = { + baseDistance * Math.sin(robotYawRad), // horizontal offset to the side + goalElevation, // vertical offset to the goal + baseDistance * Math.cos(robotYawRad) // horizontal offset forward + }; + // goalPos is relative to the robot position (which is ROBOT_POS = {0,0,0}) + + // Estimate ball flight time: assume constant speed and no air resistance. + double time = (goalDistance / ballSpeed); // seconds for the ball to reach the goal + + // Compute where the aim point must be to hit the goal given that the robot + // (launch origin) will have drifted by robotVel * time during the ball's flight. + // dx and dz are the lead-compensated horizontal aim deltas. + double dx = goalPos[0] - (ROBOT_POS[0] + robotVel[0] * time); // X: right of robot + double dz = goalPos[2] - (ROBOT_POS[2] + robotVel[2] * time); // Z: forward of robot + + // atan2 gives the compensated heading needed to reach the corrected aim point. + // Subtracting robotYawRad converts it from field heading to a yaw correction delta. double yaw = Math.atan2(dz, dx) - robotYawRad; - return Math.toDegrees(yaw); + return Math.toDegrees(yaw); // convert radians to degrees for the caller } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/outtakeUtil/RpmController.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/outtakeUtil/RpmController.java index dc028deba156..4cce43777cfa 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/outtakeUtil/RpmController.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsystems/outtakeUtil/RpmController.java @@ -4,38 +4,107 @@ import com.acmerobotics.dashboard.config.Config; +/** + * RpmController is a feedforward + proportional (FP) controller used to drive + * the shooter flywheel motors to a target RPM. + * + *

The control law is: + *

+ *   ff    = kS * sign(setpoint) + kV * setpoint   // static + velocity feedforward
+ *   pOut  = Kp * (setpoint - measured)            // proportional error correction
+ *   output = clamp(ff + pOut, -1.0, 1.0)          // combined, clamped to motor range
+ * 
+ * + *

The feedforward component ({@code kS} + {@code kV}) pre-loads the motor + * with approximately the right power so that the proportional term ({@code Kp}) + * only needs to correct small residual errors. This reduces overshoot relative + * to pure P-only control while still reaching the setpoint quickly. + * + *

Annotated with {@link Config} so gains can be tuned live in FTC Dashboard + * (note: the fields are {@code final} and will not actually be editable at + * runtime unless changed to {@code public static}). + */ @Config public class RpmController { - // Gains + // ----------------------------------------------------------------------- + // Gains (set once at construction via the constructor) + // ----------------------------------------------------------------------- + + /** + * Proportional gain: scales the RPM error (setpoint − measured) into a + * motor power correction. Larger values close the error faster but may + * cause overshoot or oscillation if too high. + */ public final double Kp; - public final double kS; //static - public final double kV; //velocity proportional + /** + * Static friction feedforward gain: a small constant power added in the + * direction of the setpoint to overcome motor and gearbox stiction before + * the proportional term fully engages. Ensures the flywheel starts moving + * even at very low RPM targets. + */ + public final double kS; // static (stiction) feedforward + + /** + * Velocity feedforward gain: scales the setpoint RPM directly into a base + * motor power estimate. The product {@code kV * setpointRPM} approximates + * the power needed to sustain the target speed, leaving only small errors + * for the P term to handle. + */ + public final double kV; // velocity proportional feedforward + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Creates an RpmController with the given FP gains. + * + * @param Kp proportional gain (RPM error → motor power) + * @param kS static-friction feedforward gain + * @param kV velocity feedforward gain (RPM → motor power) + */ public RpmController(double Kp, double kS, double kV) { - this.Kp = Kp; - this.kS = kS; - this.kV = kV; + this.Kp = Kp; // store proportional gain + this.kS = kS; // store static feedforward gain + this.kV = kV; // store velocity feedforward gain } + // ----------------------------------------------------------------------- + // Control update + // ----------------------------------------------------------------------- + /** - * Update FF + P controller and return motor power + * Computes the motor power needed to drive the flywheel from + * {@code measuredRPM} toward {@code setpointRPM} using feedforward + P + * control. + * + *

Algorithm: + *

    + *
  1. Compute RPM error = setpoint − measured.
  2. + *
  3. Compute feedforward = kS × sign(setpoint) + kV × setpoint.
  4. + *
  5. Compute P correction = Kp × error.
  6. + *
  7. Sum and clamp to [−1, 1].
  8. + *
* - * @param setpointRPM desired shooter RPM - * @param measuredRPM current shooter RPM - * @return motor power (-1 to 1) + * @param setpointRPM desired shooter flywheel speed in RPM + * @param measuredRPM current measured flywheel speed in RPM (from encoder) + * @return commanded motor power in the range [−1.0, 1.0] */ public double update(double setpointRPM, double measuredRPM) { + // RPM error: positive means we need to speed up, negative to slow down. double error = setpointRPM - measuredRPM; - // Feedforward + // Feedforward: static term ensures the motor starts moving (overcomes stiction), + // velocity term pre-loads the output with the approximate sustaining power. double ff = kS * Math.signum(setpointRPM) + kV * setpointRPM; - // P control + // Proportional correction on top of feedforward to close residual RPM error. double pOut = Kp * error; - // Clamp output + // Sum feedforward and P correction, then clamp to the valid motor power range. double output = clamp(ff + pOut, -1.0, 1.0); - return output; + return output; // return the combined motor power command } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/ActionHost.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/ActionHost.java index 22e85f6a0784..8cc6f969db6f 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/ActionHost.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/ActionHost.java @@ -1,30 +1,116 @@ package org.firstinspires.ftc.teamcode.teleop; + import com.acmerobotics.dashboard.FtcDashboard; import com.acmerobotics.dashboard.telemetry.TelemetryPacket; import com.acmerobotics.roadrunner.Action; +/** + * ActionHost manages the execution of a single Road Runner {@link Action} at + * a time in the teleop loop. + * + *

Road Runner {@link Action}s are designed for use in autonomous sequences, + * but they can also be used during teleop to perform multi-step background + * operations (e.g., raise the actuator, wait for RPM, fire, lower actuator) + * without blocking the main teleop loop. + * + *

Usage: + *

    + *
  1. Call {@link #start(Action)} to begin executing an action.
  2. + *
  3. Call {@link #update()} every loop iteration. Each call advances the + * action by one step and sends telemetry to the FTC Dashboard.
  4. + *
  5. The action ends automatically when it returns {@code false} from + * {@link Action#run(TelemetryPacket)}, or it can be cancelled early + * with {@link #abort()}.
  6. + *
  7. Check {@link #isRunning()} to decide whether to start a new action or + * allow driver inputs that would conflict with the current one.
  8. + *
+ * + *

Only one action runs at a time. Starting a new action via + * {@link #start(Action)} while one is running will replace the current action. + */ public class ActionHost { + + // ----------------------------------------------------------------------- + // State + // ----------------------------------------------------------------------- + + /** + * The currently executing {@link Action}, or {@code null} if no action is + * active. Made {@code protected} so subclasses can inspect it if needed. + */ protected Action current; + // ----------------------------------------------------------------------- + // Control methods + // ----------------------------------------------------------------------- + + /** + * Starts executing the given {@link Action}. + * If an action is already running it is discarded and replaced by the new + * one immediately. + * + * @param action the Road Runner Action to execute; must not be {@code null} + */ public void start(Action action) { - current = action; + current = action; // replace any in-progress action with the new one } + /** + * Cancels the currently running action. + * After this call {@link #isRunning()} returns {@code false} and + * {@link #update()} becomes a no-op until {@link #start(Action)} is called + * again. + */ public void abort() { - current = null; + current = null; // discard the in-progress action; any hardware it was driving keeps its last power } + /** + * Returns whether an action is currently executing. + * Use this in the teleop loop to guard button presses that would conflict + * with a running action sequence. + * + * @return {@code true} if an action is in progress; {@code false} otherwise + */ public boolean isRunning() { - return current != null; + return current != null; // non-null current ↔ action running } + // ----------------------------------------------------------------------- + // Loop update + // ----------------------------------------------------------------------- + + /** + * Advances the current action by one loop iteration. + * Must be called every robot loop tick (typically inside + * {@code BotPeriodics.handlePeriodics()}). + * + *

Each call: + *

    + *
  1. If no action is running, returns immediately.
  2. + *
  3. Creates a new {@link TelemetryPacket} for the Dashboard.
  4. + *
  5. Calls {@link Action#run(TelemetryPacket)} on the current action. + * The action executes one step and returns {@code true} if it should + * continue, {@code false} if it has completed.
  6. + *
  7. Sends the telemetry packet to FTC Dashboard so live data is visible.
  8. + *
  9. If the action is complete ({@code run()} returned {@code false}), + * clears {@link #current} to mark the host as idle.
  10. + *
+ */ public void update() { - if (current == null) return; + if (current == null) return; // no action running; nothing to do this iteration + + // Create a fresh telemetry packet; the action can write debug data into it. TelemetryPacket packet = new TelemetryPacket(); + + // Execute one step of the action. Returns true while still running, false when done. boolean stillRunning = current.run(packet); + + // Forward the packet to FTC Dashboard so live telemetry is visible during the action. FtcDashboard.getInstance().sendTelemetryPacket(packet); + if (!stillRunning) { - current = null; + current = null; // action finished; clear the host so new actions can be started } } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/Bot.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/Bot.java index 19ba45a3e5df..10ad6c6c470b 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/Bot.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/Bot.java @@ -14,294 +14,578 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; import org.firstinspires.ftc.teamcode.subsystems.Indexer; +/** + * Bot is the primary teleop controller for the robot. + * + *

It extends {@link BotPeriodics} (which owns the hardware and runs all + * per-loop subsystem updates) and adds a finite-state machine (FSM) for + * managing the five phases of a teleop match: + * + *

    + *
  1. MotifSelection – drivers choose the desired scoring motif + * (color order) before entering the Intake phase.
  2. + *
  3. Intake – robot collects three artifacts via the intake/indexer + * while the driver pre-spins the shooter when ready.
  4. + *
  5. QuickOuttake – full-drum "quick dump" mode: shooter is spun up + * and the entire drum is emptied at once in a timed blast.
  6. + *
  7. SortOuttake – sorted single-ball mode: the driver selects the + * colour of the next artifact to fire and the indexer rotates to the + * correct slot before each shot.
  8. + *
  9. Endgame – reserved for hanging or end-game actions.
  10. + *
+ * + *

Annotated with {@link Config} so tuning constants are visible in Dashboard. + */ @Config public class Bot extends BotPeriodics { - // haptics & lights + + // ----------------------------------------------------------------------- + // Haptics / lights state + // ----------------------------------------------------------------------- + + /** Latched to {@code true} once the "drum full" rumble has been sent to + * prevent repeated rumbles every loop tick after the drum fills. */ private boolean rumbledAlready = false; + + // ----------------------------------------------------------------------- + // Finite-state machine + // ----------------------------------------------------------------------- + + /** + * The five high-level states of the teleop FSM. + */ public enum FSM { + /** Pre-match: drivers select which scoring motif (color pattern) to use. */ MotifSelection, + /** Collecting artifacts via the intake/indexer assembly. */ Intake, + /** Full-drum timed dump: all three artifacts fired in one blast. */ QuickOuttake, + /** Sorted single-ball outtake: fire one artifact at a time by color. */ SortOuttake, + /** End-game phase (climbing, parking, or special actions). */ Endgame } + /** Current FSM state. Drives the {@code switch} in {@link #teleopTick()}. */ public FSM state; - public static double NON_INDEX_SPIN_TIME = 3; //seconds of full-power indexer blast + // ----------------------------------------------------------------------- + // Tuning constants (Dashboard-editable) + // ----------------------------------------------------------------------- + + /** Duration (seconds) of the full-power "non-indexed dump" spin in + * {@link #actionNonIndexedDump()}. Long enough to clear all three slots + * but short enough not to waste time after the drum is empty. */ + public static double NON_INDEX_SPIN_TIME = 3; // seconds + + /** Raw power applied to the indexer drum during the non-indexed dump blast. + * Low enough to feed balls smoothly without jamming. */ public static double FULL_BLAST_POWER = 0.25; - public static double QUICKSPIN_OUTTAKE_RPM_SCALE = 0.93; // 1.12 - public Indexer.ArtifactColor[] motif; + /** Scale factor applied to the base shooter RPM during the quick-spin outtake. + * Values < 1.0 reduce RPM slightly to trade accuracy for faster spin-up. */ + public static double QUICKSPIN_OUTTAKE_RPM_SCALE = 0.93; // proportion of full target RPM - private Indexer.ArtifactColor[] PPG = new Indexer.ArtifactColor[]{Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN}; - private Indexer.ArtifactColor[] PGP = new Indexer.ArtifactColor[]{Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE}; - private Indexer.ArtifactColor[] GPP = new Indexer.ArtifactColor[]{Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE}; + // ----------------------------------------------------------------------- + // Motif definitions + // ----------------------------------------------------------------------- + /** The motif (desired color order of artifacts) currently selected by the driver. + * Determines which slot the indexer presents first during outtake. */ + public Indexer.ArtifactColor[] motif; + + /** Purple–Purple–Green motif: two purple balls then one green. */ + private Indexer.ArtifactColor[] PPG = new Indexer.ArtifactColor[]{ + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.GREEN + }; + + /** Purple–Green–Purple motif: purple, green, purple interleaved. */ + private Indexer.ArtifactColor[] PGP = new Indexer.ArtifactColor[]{ + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.GREEN, + Indexer.ArtifactColor.PURPLE + }; + + /** Green–Purple–Purple motif: green first, then two purple. */ + private Indexer.ArtifactColor[] GPP = new Indexer.ArtifactColor[]{ + Indexer.ArtifactColor.GREEN, + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.PURPLE + }; + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Constructs a Bot, passing all hardware references to the parent class + * and initialising the FSM in the {@link FSM#MotifSelection} state. + * + * @param hardwareMap the robot's hardware map + * @param tele FTC telemetry + * @param mecanumDrive pre-built Road Runner drive + * @param gamepad1 Driver 1 raw gamepad + * @param gamepad2 Driver 2 (operator) raw gamepad + * @param twoMovement {@code true} to allow gamepad 2 to also control driving + */ public Bot(HardwareMap hardwareMap, Telemetry tele, MecanumDrive mecanumDrive, Gamepad gamepad1, Gamepad gamepad2, boolean twoMovement) { super(hardwareMap, tele, mecanumDrive, gamepad1, gamepad2, twoMovement); - state = FSM.MotifSelection; + state = FSM.MotifSelection; // always start in motif-selection so drivers confirm the motif } + // ----------------------------------------------------------------------- + // OpMode lifecycle + // ----------------------------------------------------------------------- + + /** + * Called once during the {@code init()} phase, before the start button is + * pressed. Resets the indexer color table, enables auto intake advancement, + * and stops the shooter. + */ public void teleopInit() { - indexer.initializeColors(Indexer.ArtifactColor.EMPTY); - indexer.setIntaking(true); - state = FSM.MotifSelection; - outtake.stop(); + indexer.initializeColors(Indexer.ArtifactColor.EMPTY); // mark every slot as empty at start + indexer.setIntaking(true); // enable the indexer's auto-advance-on-intake logic + state = FSM.MotifSelection; // reset FSM to motif-selection in case of re-init + outtake.stop(); // ensure shooter is off before the match begins } + /** + * Called once immediately after the start button is pressed. + * Lowers the actuator to the "down / intake" position and moves the + * indexer to its zero (home) slot so intake can begin immediately. + */ public void teleopStart(){ - actuator.down(); - indexer.moveTo(Indexer.IndexerState.zero); + actuator.down(); // bring the actuator to the loading position + indexer.moveTo(Indexer.IndexerState.zero); // rotate drum so slot 0 faces the intake port } + // ----------------------------------------------------------------------- + // Main loop tick + // ----------------------------------------------------------------------- + + /** + * Called every loop iteration from the OpMode's while loop. + * Runs all periodic subsystem updates first, then delegates to the + * appropriate FSM state handler. + */ public void teleopTick() { - handlePeriodics(); + handlePeriodics(); // run all subsystem periodic updates (inherited from BotPeriodics) + + // Route to the correct state handler based on the current FSM state. switch (state) { case MotifSelection: + // Wait for the driver to press a face button choosing the motif. if (g2.wasJustPressed(GamepadKeys.Button.X)){ - motif = PPG; - indexer.prepareQuickspin(motif); - state = FSM.Intake; + motif = PPG; // select PPG motif + indexer.prepareQuickspin(motif); // rotate drum to the first motif slot + state = FSM.Intake; // transition to intake phase } if (g2.wasJustPressed(GamepadKeys.Button.Y)){ - motif = PGP; + motif = PGP; // select PGP motif indexer.prepareQuickspin(motif); state = FSM.Intake; } if (g2.wasJustPressed(GamepadKeys.Button.B)){ - motif = GPP; + motif = GPP; // select GPP motif indexer.prepareQuickspin(motif); state = FSM.Intake; } break; case Intake: - handleIntakeState(); + handleIntakeState(); // handle intake controls and state transitions break; case QuickOuttake: - handleQuickOuttakeState(); + handleQuickOuttakeState(); // handle quick-dump controls break; case SortOuttake: - handleSortOuttakeState(); + handleSortOuttakeState(); // handle sorted single-ball firing controls break; case Endgame: - handleEndgameState(); + handleEndgameState(); // handle endgame controls break; } } + // ----------------------------------------------------------------------- + // FSM state handlers + // ----------------------------------------------------------------------- + + /** + * Handles driver inputs and logic while in the {@link FSM#Intake} state. + * + *

Key bindings (gamepad 2): + *

+ * + *

Also handles the "drum full" rumble notification to both gamepads. + */ // MAINLINE HANDLERS private void handleIntakeState() { double leftTrigger = g2.getTrigger(GamepadKeys.Trigger.LEFT_TRIGGER); - //double rightTrigger = g2.getTrigger(GamepadKeys.Trigger.RIGHT_TRIGGER); - // Press-and-hold right bumper to spin up shooter while in Intake + // Right bumper held: begin spinning up the shooter and transition to quick outtake. if (!actionHost.isRunning()) { if (g2.gamepad.right_bumper) { - rangeRequested = true; - state = FSM.QuickOuttake; - applyPreSpinRPM(); + rangeRequested = true; // start requesting range / turn-correction from aimer + state = FSM.QuickOuttake; // switch FSM to quick-outtake mode + applyPreSpinRPM(); // command shooter to pre-spin target RPM } else { - outtake.stop(); - rangeRequested = false; + outtake.stop(); // no bumper held: ensure shooter is off + rangeRequested = false; // stop requesting range updates } } - + // Left trigger: manually run the intake in addition to any continuous intake. if (leftTrigger > TeleopConstants.Gamepad.TRIGGER_DEADZONE) intake.run(); else intake.stop(); - //if (rightTrigger > TRIGGER_DEADZONE) intake.runBackwards(); - //else intake.stop(); - - if (g2.wasJustPressed(GamepadKeys.Button.DPAD_RIGHT)) indexer.moveTo(indexer.getState().next()); + // DPAD_RIGHT: manually step the indexer to the next slot. + if (g2.wasJustPressed(GamepadKeys.Button.DPAD_RIGHT)) + indexer.moveTo(indexer.getState().next()); + // X: debug override – force all three slots to the PPG colour assignment. if(g2.wasJustPressed(GamepadKeys.Button.X)) - indexer.initializeColors(Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.PURPLE, Indexer.ArtifactColor.GREEN); + indexer.initializeColors( + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.GREEN + ); + + // A: jump to QuickOuttake without pre-spinning. if (g2.wasJustPressed(GamepadKeys.Button.A)) state = FSM.QuickOuttake; + + // B: jump to SortOuttake (disable intake auto-advance so drum doesn't rotate during aiming). if (g2.wasJustPressed(GamepadKeys.Button.B)){ state = FSM.SortOuttake; - indexer.setIntaking(false); - indexer.moveTo(indexer.getState()); + indexer.setIntaking(false); // stop indexer auto-advance + indexer.moveTo(indexer.getState()); // hold current slot } + + // DPAD_UP: re-prepare the quick-spin ordering for the current motif. if(g2.wasJustPressed(GamepadKeys.Button.DPAD_UP)) - indexer.prepareQuickspin(new Indexer.ArtifactColor[]{Indexer.ArtifactColor.GREEN, Indexer.ArtifactColor.PURPLE,Indexer.ArtifactColor.PURPLE}); + indexer.prepareQuickspin(new Indexer.ArtifactColor[]{ + Indexer.ArtifactColor.GREEN, + Indexer.ArtifactColor.PURPLE, + Indexer.ArtifactColor.PURPLE + }); + + // Y: jump to endgame mode. if (g2.wasJustPressed(GamepadKeys.Button.Y)) state = FSM.Endgame; + + // Second DPAD_UP check prepares using the driver-selected motif. if(g2.wasJustPressed(GamepadKeys.Button.DPAD_UP)) indexer.prepareQuickspin(motif); - if(indexer.isFull() && !rumbledAlready && !g1.gamepad.isRumbling() && !g2.gamepad.isRumbling()){ - g1.gamepad.rumbleBlips(TeleopConstants.Gamepad.FULL_WARNING_RUMBLES); - g2.gamepad.rumbleBlips(TeleopConstants.Gamepad.FULL_WARNING_RUMBLES); - rumbledAlready = true; + // Drum-full notification: rumble both gamepads once when the drum fills. + if(indexer.isFull() && !rumbledAlready + && !g1.gamepad.isRumbling() && !g2.gamepad.isRumbling()){ + g1.gamepad.rumbleBlips(TeleopConstants.Gamepad.FULL_WARNING_RUMBLES); // alert driver 1 + g2.gamepad.rumbleBlips(TeleopConstants.Gamepad.FULL_WARNING_RUMBLES); // alert driver 2 + rumbledAlready = true; // latch so we only rumble once per fill cycle } } + /** + * Overrides the parent's alliance selection handler to add gamepad LED + * color feedback when an alliance is selected. + * + *

Key bindings (gamepad 1): + *

+ */ protected void handleAllianceSelection() { if (g1.wasJustPressed(GamepadKeys.Button.BACK)) { - aprilTag.setPipeline(0); + aprilTag.setPipeline(0); // pipeline 0 = blue alliance goal tag aimer.setBlueTarget(); + // Set gamepad 1 LED to blue for the configured duration. g1.gamepad.setLedColor(0, 0, 1, TeleopConstants.Gamepad.GAMEPAD_LIGHT_COLOR_DURATION); colorGoalSelected = "Blue"; } if (g1.wasJustPressed(GamepadKeys.Button.START)) { - aprilTag.setPipeline(1); + aprilTag.setPipeline(1); // pipeline 1 = red alliance goal tag aimer.setRedTarget(); + // Set gamepad 1 LED to red for the configured duration. g1.gamepad.setLedColor(1, 0, 0, TeleopConstants.Gamepad.GAMEPAD_LIGHT_COLOR_DURATION); colorGoalSelected = "Red"; } } + /** + * Handles driver inputs while in the {@link FSM#QuickOuttake} state. + * + *

Key bindings (gamepad 2): + *

+ */ private void handleQuickOuttakeState() { - // Allow press-and-hold pre-spin while in QuickOuttake (before running actions) + // Allow press-and-hold pre-spin while waiting to confirm the dump. if (!actionHost.isRunning()) { if (g2.gamepad.right_bumper) { - applyPreSpinRPM(); - rangeRequested = true; + applyPreSpinRPM(); // keep shooter at pre-spin RPM + rangeRequested = true; // keep requesting range/turn correction } else { rangeRequested = false; - outtake.stop(); + outtake.stop(); // bumper released: stop shooter } } + // X: fire the non-indexed dump action; reset rumble latch and return to Intake after. if (!actionHost.isRunning() && g2.wasJustPressed(GamepadKeys.Button.X)) { - actionHost.start(actionNonIndexedDump()); - rumbledAlready = false; - state = FSM.Intake; + actionHost.start(actionNonIndexedDump()); // start the multi-step dump sequence + rumbledAlready = false; // allow re-triggering the rumble for the next fill cycle + state = FSM.Intake; // FSM transitions back to Intake when the action finishes } + + // DPAD_UP: re-prepare the quick-spin ordering for the current motif. if(g2.wasJustPressed(GamepadKeys.Button.DPAD_UP)) indexer.prepareQuickspin(motif); + + // BACK: abort whatever action is currently running. if (g2.wasJustPressed(GamepadKeys.Button.BACK)) { - actionHost.abort(); + actionHost.abort(); // cancel the in-progress action immediately } + + // A: cancel quick outtake; re-enable intake auto-advance and return to Intake. if (g2.wasJustPressed(GamepadKeys.Button.A)) { state = FSM.Intake; - indexer.setIntaking(true); - rumbledAlready = false; + indexer.setIntaking(true); // re-enable auto-advance so new artifacts can be indexed + rumbledAlready = false; // reset rumble latch } } + /** + * Handles driver inputs while in the {@link FSM#SortOuttake} state. + * + *

Key bindings (gamepad 2): + *

+ */ private void handleSortOuttakeState() { if (!actionHost.isRunning()) { + // X: fire the next available green artifact. if (g2.wasJustPressed(GamepadKeys.Button.X)) { - actionHost.start(actionFireGreen()); + actionHost.start(actionFireGreen()); // start green-shot sequence } + // Y: fire the next available purple artifact. if (g2.wasJustPressed(GamepadKeys.Button.Y)) { - actionHost.start(actionFirePurple()); + actionHost.start(actionFirePurple()); // start purple-shot sequence } } + // BACK: abort any currently running shot sequence. if (g2.wasJustPressed(GamepadKeys.Button.BACK)) { - actionHost.abort(); + actionHost.abort(); // cancel the in-progress shot action } + // A: exit sort-outtake; re-enable intake and return to collecting. if (g2.wasJustPressed(GamepadKeys.Button.A)) { - indexer.setIntaking(true); + indexer.setIntaking(true); // re-enable auto-advance on the indexer state = FSM.Intake; } } - + /** + * Handles driver inputs while in the {@link FSM#Endgame} state. + * + *

Key bindings (gamepad 2): + *

+ */ private void handleEndgameState() { + // A: leave endgame and resume normal intake. if (g2.wasJustPressed(GamepadKeys.Button.A)) { state = FSM.Intake; - - indexer.setIntaking(true); + indexer.setIntaking(true); // re-enable auto-advance so the drum can fill again } } + // ----------------------------------------------------------------------- + // RPM helper + // ----------------------------------------------------------------------- + + /** + * Commands the shooter to spin up to the "quick-spin" RPM (base target RPM + * scaled by {@link #QUICKSPIN_OUTTAKE_RPM_SCALE}). + * Called while the driver holds the right bumper to pre-heat the flywheel. + */ private void applyPreSpinRPM() { - outtake.set(getTargetRPM()*QUICKSPIN_OUTTAKE_RPM_SCALE); // RPM mode: set shooter target RPM + outtake.set(getTargetRPM() * QUICKSPIN_OUTTAKE_RPM_SCALE); // RPM mode: set shooter target RPM } + // ----------------------------------------------------------------------- + // Action builders + // ----------------------------------------------------------------------- + + /** + * Builds the "non-indexed dump" Road Runner {@link Action} sequence. + * + *

This is the quick-outtake path: all three artifacts are fired at once + * without sorting by colour. The sequence: + *

    + *
  1. Raise actuator to the "quick" (lower) up position.
  2. + *
  3. Set shooter to quick-spin RPM.
  4. + *
  5. Wait until the shooter is within 50 RPM of target.
  6. + *
  7. Run the indexer drum at {@link #FULL_BLAST_POWER} for + * {@link #NON_INDEX_SPIN_TIME} seconds to dump all artifacts.
  8. + *
  9. Stop the indexer and shooter.
  10. + *
  11. Lower the actuator.
  12. + *
  13. Re-enable intake auto-advance.
  14. + *
  15. Reset all slot colours to EMPTY.
  16. + *
  17. Return the indexer to slot zero ready for the next intake cycle.
  18. + *
+ * + * @return the fully-constructed dump {@link Action} + */ private Action actionNonIndexedDump() { - final double rpm = getTargetRPM() * QUICKSPIN_OUTTAKE_RPM_SCALE; + final double rpm = getTargetRPM() * QUICKSPIN_OUTTAKE_RPM_SCALE; // pre-compute RPM to use return new SequentialAction( - new InstantAction(actuator::upQuick), - new InstantAction(() -> outtake.set(rpm)), + new InstantAction(actuator::upQuick), // 1. raise actuator quickly + new InstantAction(() -> outtake.set(rpm)), // 2. start shooter spin-up new Action() { @Override public boolean run(TelemetryPacket packet) { - return !outtake.inRange(50); + return !outtake.inRange(50); // 3. block until RPM is within 50 of target } }, - new InstantAction(() -> indexer.setIndexerPower(FULL_BLAST_POWER)), - new SleepAction(NON_INDEX_SPIN_TIME), - new InstantAction(indexer::stopIndexerPower), - new InstantAction(outtake::stop), - new InstantAction(actuator::down), - new InstantAction(() -> indexer.setIntaking(true)), - new InstantAction(indexer::initializeColors), - new InstantAction(() -> indexer.moveTo(Indexer.IndexerState.zero)) + new InstantAction(() -> indexer.setIndexerPower(FULL_BLAST_POWER)), // 4. blast drum + new SleepAction(NON_INDEX_SPIN_TIME), // 5. timed blast duration + new InstantAction(indexer::stopIndexerPower), // 6. stop drum + new InstantAction(outtake::stop), // 7. stop shooter + new InstantAction(actuator::down), // 8. lower actuator + new InstantAction(() -> indexer.setIntaking(true)), // 9. re-enable intake + new InstantAction(indexer::initializeColors), // 10. mark all slots EMPTY + new InstantAction(() -> indexer.moveTo(Indexer.IndexerState.zero)) // 11. home indexer ); } + /** + * Builds the "fire green artifact" Road Runner {@link Action} sequence. + * + *

Finds the best slot containing a GREEN artifact, rotates the indexer + * to that slot, spins up the shooter, and fires when in range. + * If no green artifact is loaded, returns a no-op action. + * + *

Sequence: + *

    + *
  1. Disable intake auto-advance.
  2. + *
  3. Lower the actuator.
  4. + *
  5. Rotate indexer to the green slot.
  6. + *
  7. Spin up shooter to target RPM.
  8. + *
  9. Wait until within 100 RPM of target.
  10. + *
  11. Raise actuator to indexed position.
  12. + *
  13. Wait 1 second for the artifact to exit.
  14. + *
  15. Mark that slot as EMPTY.
  16. + *
  17. Stop shooter and lower actuator.
  18. + *
+ * + * @return the green-shot {@link Action}, or a no-op if no green slot exists + */ private Action actionFireGreen() { + // Find the indexer slot that best matches a GREEN artifact. final Indexer.IndexerState slot = indexer.findBestSlotForColor(Indexer.ArtifactColor.GREEN); if (slot == null) { + // No green artifact loaded: return an empty no-op action. return new InstantAction(() -> {}); } - final double rpm = getTargetRPM(); + final double rpm = getTargetRPM(); // read target RPM now, not at action-run time return new SequentialAction( - new InstantAction(() -> indexer.setIntaking(false)), - new InstantAction(actuator::down), - new InstantAction(() -> indexer.moveTo(slot, true)), - new InstantAction(() -> outtake.set(rpm)), - // wait for shooter RPM to be within 100 instead of fixed spinup + new InstantAction(() -> indexer.setIntaking(false)), // 1. stop auto-advance + new InstantAction(actuator::down), // 2. lower actuator (clear path) + new InstantAction(() -> indexer.moveTo(slot, true)), // 3. rotate to green slot + new InstantAction(() -> outtake.set(rpm)), // 4. spin up shooter + // 5. wait for shooter to reach target RPM within 100 RPM new Action() { @Override public boolean run(TelemetryPacket p) { - return !outtake.inRange(100.0); + return !outtake.inRange(100.0); // returns true while NOT in range (keep waiting) } }, - new InstantAction(actuator::upIndexed), - new SleepAction(1), + new InstantAction(actuator::upIndexed), // 6. raise actuator to fire position + new SleepAction(1), // 7. allow 1 s for artifact to exit + // 8. mark the fired slot as empty so the indexer knows it's clear new InstantAction(() -> indexer.assignSlotColor(slot, Indexer.ArtifactColor.EMPTY) ), - new InstantAction(outtake::stop), - new InstantAction(actuator::down) + new InstantAction(outtake::stop), // 9. stop shooter + new InstantAction(actuator::down) // 10. lower actuator back to intake pos ); } + /** + * Builds the "fire purple artifact" Road Runner {@link Action} sequence. + * + *

Finds the best slot containing a PURPLE artifact, rotates the indexer + * to that slot, spins up the shooter, and fires when in range. + * If no purple artifact is loaded, returns a no-op action. + * + *

Sequence is the same as {@link #actionFireGreen()} but targets PURPLE. + * + * @return the purple-shot {@link Action}, or a no-op if no purple slot exists + */ private Action actionFirePurple() { + // Find the indexer slot that best matches a PURPLE artifact. final Indexer.IndexerState slot = indexer.findBestSlotForColor(Indexer.ArtifactColor.PURPLE); if (slot == null) { + // No purple artifact loaded: return an empty no-op action. return new InstantAction(() -> { }); } - final double rpm = getTargetRPM(); + final double rpm = getTargetRPM(); // capture current RPM target return new SequentialAction( - new InstantAction(actuator::down), - new InstantAction(() -> indexer.moveTo(slot, true)), + new InstantAction(actuator::down), // 1. ensure actuator is lowered + new InstantAction(() -> indexer.moveTo(slot, true)), // 2. rotate to purple slot - new InstantAction(() -> outtake.set(rpm)), - // wait for shooter RPM to be within 100 instead of fixed spinup + new InstantAction(() -> outtake.set(rpm)), // 3. spin up shooter + // 4. wait for shooter RPM to be within 100 of target new Action() { @Override public boolean run(TelemetryPacket p) { - return !outtake.inRange(100.0); + return !outtake.inRange(100.0); // returns true while NOT in range (keep waiting) } }, - new InstantAction(actuator::upIndexed), - new SleepAction(1), + new InstantAction(actuator::upIndexed), // 5. raise actuator to fire position + new SleepAction(1), // 6. allow 1 s for artifact to exit + // 7. mark the fired slot as empty new InstantAction(() -> indexer.assignSlotColor(slot, Indexer.ArtifactColor.EMPTY) ), - new InstantAction(outtake::stop), - new InstantAction(actuator::down) + new InstantAction(outtake::stop), // 8. stop shooter + new InstantAction(actuator::down) // 9. lower actuator ); } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/BotPeriodics.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/BotPeriodics.java index 8ce26899e589..6e8df30dfa45 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/BotPeriodics.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/BotPeriodics.java @@ -16,195 +16,413 @@ import org.firstinspires.ftc.teamcode.subsystems.Movement; import org.firstinspires.ftc.teamcode.subsystems.Outtake; +/** + * BotPeriodics is the base class for all teleop bot controllers. + * + *

It holds every hardware subsystem as a protected field and implements the + * per-loop "periodic" handlers that must run every iteration regardless of the + * current game state: + *

+ * + *

Subclasses (e.g., {@link Bot}) override {@link #handleAllianceSelection()} + * to add extra LED/haptics logic, and add game-state FSM code on top. + * + *

Annotated with {@link Config} so {@link #targetRPM} and any future static + * fields are editable via FTC Dashboard. + */ @Config public class BotPeriodics { + + // ----------------------------------------------------------------------- + // Subsystems (protected so subclasses can access them directly) + // ----------------------------------------------------------------------- + + /** Conveyor / intake roller subsystem. */ protected final Intake intake; + + /** Three-slot indexer drum subsystem. */ protected final Indexer indexer; + + /** Actuator (launch ramp or ball-gate) subsystem. */ protected final Actuator actuator; + + /** Flywheel shooter (outtake) subsystem. */ protected final Outtake outtake; + + /** Mecanum drivetrain subsystem. */ protected final Movement movement; + + /** Limelight AprilTag scanner subsystem. */ protected final AprilTag aprilTag; + + /** Localization-based heading aimer. */ protected final Aimer aimer; + + /** Road Runner MecanumDrive used for pose estimation and field-centric heading. */ protected final MecanumDrive drive; + + // ----------------------------------------------------------------------- + // Gamepad wrappers (FTCLib – provide edge-detection on button presses) + // ----------------------------------------------------------------------- + + /** Driver 1 gamepad with edge-detection helpers. */ protected final GamepadEx g1; + + /** Driver 2 (operator) gamepad with edge-detection helpers. */ protected final GamepadEx g2; + + /** FTC telemetry object for writing Driver Station / Dashboard output. */ protected final Telemetry telemetry; + + // ----------------------------------------------------------------------- + // Aiming / vision state + // ----------------------------------------------------------------------- + + /** Bearing turn-correction power from the last AprilTag/aimer update. + * Blended into the driver's rotation input to keep the robot aimed at the goal. */ protected double bearingTurnCorrection = 0; + + /** Background Road Runner Action executor (one action at a time during teleop). */ protected ActionHost actionHost; - // camera vision + + /** Whether field-centric drive mode is currently active. */ protected boolean fieldCentric = false; + + /** When {@code true} the aimer runs every {@link #AIM_UPDATE_INTERVAL_MS} ms + * to maintain a continuous lock on the goal. The robot's heading correction + * is applied to the drive command as long as this flag is set. */ protected boolean continuousAprilTagLock = false; + + /** System time (ms) of the last aimer update tick. Used to rate-limit calls to + * {@link Aimer#calculateLocalizedTurnPower()} to {@link #AIM_UPDATE_INTERVAL_MS}. */ protected long lastAimUpdate = 0; + + /** The turn-correction value from the previous aimer update. Held between + * updates so the motor command is not zero between update periods. */ protected double lastTurnCorrection = 0.0; + + /** Current turn correction applied to the drive command this loop iteration. */ protected double turnCorrection = 0.0; + + /** Human-readable alliance colour selected by the drivers ("Red" or "Blue"). + * Shown in telemetry so the team can confirm the correct alliance is configured. */ protected String colorGoalSelected = ""; + + /** Whether the driver has requested an RPM spin-up/range calculation this tick. */ protected boolean rangeRequested = false; - protected double[] targetData = {0,0,0}; + /** Most recent aimer output: {@code {turnPower, range, bearing}}. + * Index 0 = turn power, Index 1 = range (inches), Index 2 = bearing (degrees). */ + protected double[] targetData = {0, 0, 0}; + + /** When {@code true} the intake runs at slow speed continuously in the background + * (passive intake). Toggled by the DPAD_DOWN button on gamepad 2. */ protected boolean continuousIntake = true; + // ----------------------------------------------------------------------- + // Shooter RPM configuration + // ----------------------------------------------------------------------- + + /** + * Target RPM for the shooter flywheel. Updated dynamically from the range + * regression when aiming is active; can also be set manually via Dashboard. + */ public static double targetRPM = 3800; + + /** + * Minimum interval (ms) between aimer recalculations. + * At 20 ms = 50 Hz: fast enough for smooth correction without overloading + * the control loop with expensive trigonometry every single tick. + */ protected static final long AIM_UPDATE_INTERVAL_MS = 20; + // ----------------------------------------------------------------------- + // Drive mode + // ----------------------------------------------------------------------- + + /** + * {@code true} = use gamepad 2 for drive inputs; {@code false} = gamepad 1. + * Allows a "two-movement-mode" where the second driver also controls driving + * (e.g., for practice or single-player competitions). + */ protected boolean twoMovementMode; + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /** + * Constructs a BotPeriodics and initialises every hardware subsystem. + * + * @param hardwareMap the robot's hardware map used to locate all devices + * @param tele FTC telemetry object; output goes to Driver Station and Dashboard + * @param mecanumDrive pre-constructed Road Runner drive for pose estimation + * @param gamepad1 the Driver 1 raw gamepad + * @param gamepad2 the Driver 2 (operator) raw gamepad + * @param useMovement when {@code true} gamepad 2 also controls driving + */ public BotPeriodics(HardwareMap hardwareMap, Telemetry tele, MecanumDrive mecanumDrive, Gamepad gamepad1, Gamepad gamepad2, boolean useMovement) { - intake = new Intake(hardwareMap); - indexer = new Indexer(hardwareMap); + // Construct all hardware subsystems from the hardware map. + intake = new Intake(hardwareMap); + indexer = new Indexer(hardwareMap); actuator = new Actuator(hardwareMap); - outtake = new Outtake(hardwareMap, Outtake.Mode.RPM); - drive = mecanumDrive; + outtake = new Outtake(hardwareMap, Outtake.Mode.RPM); // use RPM-mode for closed-loop control + drive = mecanumDrive; movement = new Movement(hardwareMap, drive); aprilTag = new AprilTag(hardwareMap, tele); - aimer = new Aimer(drive); + aimer = new Aimer(drive); + + // Wrap raw gamepads with FTCLib GamepadEx to get edge-detection helpers. g1 = new GamepadEx(gamepad1); g2 = new GamepadEx(gamepad2); - actionHost = new ActionHost(); + + actionHost = new ActionHost(); // background action executor telemetry = tele; - twoMovementMode = useMovement; + twoMovementMode = useMovement; // remember whether gamepad 2 also drives } - + + // ----------------------------------------------------------------------- + // Main periodic handler + // ----------------------------------------------------------------------- + + /** + * Runs all per-loop periodic tasks. Must be called at the top of every + * teleop iteration (before the subclass's state-machine switch). + * + *

Order of operations: + *

    + *
  1. Read button state from both gamepads (edge detection refresh).
  2. + *
  3. Toggle continuous intake on DPAD_DOWN (gamepad 2).
  4. + *
  5. Read both gamepads' triggers and decide intake direction/speed.
  6. + *
  7. Handle heading lock, drive movement, alliance selection, telemetry.
  8. + *
  9. Tick subsystem periodic updates: indexer, outtake, action host, pose estimator.
  10. + *
  11. If the outtake target RPM is nonzero, keep refreshing it at the current target.
  12. + *
  13. If aiming is requested, periodically recalculate the turn correction and target RPM.
  14. + *
+ */ protected void handlePeriodics() { + // Refresh button edge-detection state for both gamepads. g1.readButtons(); g2.readButtons(); + // DPAD_DOWN on gamepad 2 toggles passive (slow continuous) intake on/off. if(g2.wasJustPressed(GamepadKeys.Button.DPAD_DOWN)) continuousIntake = !continuousIntake; - double leftTrigger = g1.getTrigger(GamepadKeys.Trigger.LEFT_TRIGGER); + // Read trigger values from both gamepads. + double leftTrigger = g1.getTrigger(GamepadKeys.Trigger.LEFT_TRIGGER); double leftTrigger2 = g2.getTrigger(GamepadKeys.Trigger.LEFT_TRIGGER); - double rightTrigger = g1.getTrigger(GamepadKeys.Trigger.RIGHT_TRIGGER); + double rightTrigger = g1.getTrigger(GamepadKeys.Trigger.RIGHT_TRIGGER); double rightTrigger2 = g2.getTrigger(GamepadKeys.Trigger.RIGHT_TRIGGER); - boolean leftDown = leftTrigger > TeleopConstants.Gamepad.TRIGGER_DEADZONE || leftTrigger2 > TeleopConstants.Gamepad.TRIGGER_DEADZONE; - boolean rightDown = rightTrigger > TeleopConstants.Gamepad.TRIGGER_DEADZONE || rightTrigger2 > TeleopConstants.Gamepad.TRIGGER_DEADZONE; + // True if either driver is pressing the left (run) or right (reverse) trigger. + boolean leftDown = leftTrigger > TeleopConstants.Gamepad.TRIGGER_DEADZONE + || leftTrigger2 > TeleopConstants.Gamepad.TRIGGER_DEADZONE; + boolean rightDown = rightTrigger > TeleopConstants.Gamepad.TRIGGER_DEADZONE + || rightTrigger2 > TeleopConstants.Gamepad.TRIGGER_DEADZONE; + // Check whether the indexer drum is aligned with the intake port. boolean inRange = indexer.isWithinTargetDegrees(5); + // Intake control priority: left trigger > right trigger > passive slow > stop. if(leftDown){ - intake.run(); + intake.run(); // full-speed forward intake } - else if(rightDown) intake.runBackwards(); - else if(continuousIntake) intake.runSlow(); - else intake.stop(); + else if(rightDown) intake.runBackwards(); // reverse intake to unjam + else if(continuousIntake) intake.runSlow(); // passive slow intake in background + else intake.stop(); // all triggers released and passive disabled - // driver one (constant - handleAprilTagLock(); - handleMovement(); - handleAllianceSelection(); + // --- driver 1 periodic handlers (run every loop) --- + handleAprilTagLock(); // update auto-aim heading lock state + handleMovement(); // send joystick inputs to the drive motors + handleAllianceSelection(); // check for alliance selection button presses - handleTelemetry(); + handleTelemetry(); // push diagnostic data to Driver Station / Dashboard - indexer.update(); - outtake.periodic(); - actionHost.update(); - drive.updatePoseEstimate(); + // Tick all subsystems that need a per-loop update. + indexer.update(); // run indexer servo PD control and color sensing + outtake.periodic(); // update flywheel RPM measurement and controller + actionHost.update(); // advance any running teleop Action by one step + drive.updatePoseEstimate(); // integrate odometry/IMU into the pose estimate + // If the shooter is spinning, continuously refresh the RPM setpoint. if (outtake.getTargetRPM() > 0) { - outtake.set(targetRPM * Bot.QUICKSPIN_OUTTAKE_RPM_SCALE); + outtake.set(targetRPM * Bot.QUICKSPIN_OUTTAKE_RPM_SCALE); // hold RPM at the dialed-in target } + // If range feedback is requested or continuous lock is active, periodically + // recalculate the bearing turn correction and the range-dependent target RPM. if(rangeRequested || continuousAprilTagLock){ long now = System.currentTimeMillis(); + // Rate-limit the aimer recalculation to AIM_UPDATE_INTERVAL_MS. if (now - lastAimUpdate >= AIM_UPDATE_INTERVAL_MS) { lastAimUpdate = now; - targetData = aimer.calculateLocalizedTurnPower(); - lastTurnCorrection = targetData[0]; - targetRPM = outtake.getRegressionRPM(targetData[1]); - bearingTurnCorrection = targetData[2]; + targetData = aimer.calculateLocalizedTurnPower(); // {turnPower, range, bearing} + lastTurnCorrection = targetData[0]; // save turn correction for the drive command + targetRPM = outtake.getRegressionRPM(targetData[1]); // set RPM from range regression + bearingTurnCorrection = targetData[2]; // save bearing for telemetry } - turnCorrection = lastTurnCorrection; + turnCorrection = lastTurnCorrection; // apply the most recent turn correction to drive } } - // Periodic Handlers + + // ----------------------------------------------------------------------- + // Periodic handler helpers + // ----------------------------------------------------------------------- + + /** + * Writes diagnostic data for all subsystems to the Driver Station + * telemetry panel and FTC Dashboard. Called once per loop iteration. + */ protected void handleTelemetry() { - telemetry.addData("Field Centric", fieldCentric); + telemetry.addData("Field Centric", fieldCentric); // display current drive mode telemetry.addData("Indexer State", "%s -> %s", - indexer.getState(), indexer.getState().next()); + indexer.getState(), indexer.getState().next()); // current and next slot telemetry.addData("Indexer Voltages", "Target: %.3f , Actual: %.3f", - indexer.getTargetVoltage(), indexer.getVoltage()); - telemetry.addData("Outtake RPM", outtake.getRPM()); - telemetry.addData("Target RMP", outtake.getTargetRPM()); - telemetry.addData("Actuator up?", actuator.isActivated()); - telemetry.addData("Indexer Loaded?", indexer.isLoaded()); - telemetry.addData("April Lock", continuousAprilTagLock); - telemetry.addData("Bot Range", targetData[1]); - telemetry.addData("Alliance selected", colorGoalSelected); - telemetry.addData("Turn Correction:", turnCorrection); - telemetry.addData("Intake power: ", intake.getPower()); - telemetry.addData("Last Turn Correction", lastTurnCorrection); + indexer.getTargetVoltage(), indexer.getVoltage()); // servo position tracking + telemetry.addData("Outtake RPM", outtake.getRPM()); // measured flywheel RPM + telemetry.addData("Target RMP", outtake.getTargetRPM()); // commanded RPM setpoint + telemetry.addData("Actuator up?", actuator.isActivated()); // actuator position flag + telemetry.addData("Indexer Loaded?", indexer.isLoaded()); // drum load state + telemetry.addData("April Lock", continuousAprilTagLock); // heading lock active? + telemetry.addData("Bot Range", targetData[1]); // distance to goal (inches) + telemetry.addData("Alliance selected", colorGoalSelected); // selected alliance colour + telemetry.addData("Turn Correction:", turnCorrection); // current yaw correction + telemetry.addData("Intake power: ", intake.getPower()); // intake motor power + telemetry.addData("Last Turn Correction", lastTurnCorrection); // previous yaw correction + // Print per-slot colour and positional error for all three indexer slots. for (Indexer.IndexerState s : Indexer.IndexerState.values()) { telemetry.addData( "Slot " + s.index, "%s (err=%.1f°)", - indexer.getColorAt(s), - indexer.debugSlotErrorDeg(s) + indexer.getColorAt(s), // color stored in this slot + indexer.debugSlotErrorDeg(s) // how far off the slot is from its target angle ); } - telemetry.update(); + telemetry.update(); // flush all data to the Driver Station display } + /** + * Checks for alliance selection button presses from driver 1 and updates + * the Limelight pipeline, the aimer's target pose, and the telemetry label. + * + * + * + *

Subclasses may override this to add LED/haptic feedback on selection. + */ protected void handleAllianceSelection() { if (g1.wasJustPressed(GamepadKeys.Button.BACK)) { - aprilTag.setPipeline(0); - aimer.setBlueTarget(); - colorGoalSelected = "Blue"; + aprilTag.setPipeline(0); // pipeline 0 = blue goal AprilTag + aimer.setBlueTarget(); // aim toward the blue alliance goal + colorGoalSelected = "Blue"; // update telemetry label } if (g1.wasJustPressed(GamepadKeys.Button.START)) { - aprilTag.setPipeline(1); - aimer.setRedTarget(); - colorGoalSelected = "Red"; + aprilTag.setPipeline(1); // pipeline 1 = red goal AprilTag + aimer.setRedTarget(); // aim toward the red alliance goal + colorGoalSelected = "Red"; // update telemetry label } } + /** + * Reads stick inputs from the active driver gamepad and forwards them to + * the drive subsystem, applying the current turn correction from the aimer. + * + *

If {@link #twoMovementMode} is {@code true} the drive inputs come from + * gamepad 2 (operator) instead of gamepad 1 (driver). This is useful for + * single-player practice sessions. + */ protected void handleMovement() { + // Read joystick axes from the appropriate gamepad. + double lx = g1.getLeftX(); // driver 1 left stick X (strafe) + double ly = g1.getLeftY(); // driver 1 left stick Y (forward) + double rx = g1.getRightX(); // driver 1 right stick X (rotate) - double lx = g1.getLeftX(); - double ly = g1.getLeftY(); - double rx = g1.getRightX(); if(twoMovementMode){ - lx = g2.getLeftX(); - ly = g2.getLeftY(); - rx = g2.getRightX(); + // Override with gamepad 2 inputs in two-movement mode. + lx = g2.getLeftX(); + ly = g2.getLeftY(); + rx = g2.getRightX(); } + + // Send to the drive subsystem in the appropriate mode. if (fieldCentric) movement.teleopTickFieldCentric(lx, ly, rx, turnCorrection, true); - else movement.teleopTick(lx, ly, rx, turnCorrection); + else movement.teleopTick(lx, ly, rx, turnCorrection); // robot-centric mode } + /** + * Handles buttons that toggle the continuous AprilTag heading lock and + * trigger a localizer re-zero. + * + *

+ * + *

When the lock is disabled {@link #turnCorrection} is set to 0 so the + * robot doesn't keep spinning based on a stale correction value. + */ protected void handleAprilTagLock() { - - // Toggle continuous lock + // A: enable continuous heading lock toward the goal. if (g1.wasJustPressed(GamepadKeys.Button.A)) { continuousAprilTagLock = true; - g1.gamepad.rumbleBlips(2); + g1.gamepad.rumbleBlips(2); // 2 blips = lock engaged } + // B: disable continuous heading lock. if (g1.wasJustPressed(GamepadKeys.Button.B)) { continuousAprilTagLock = false; - g1.gamepad.rumbleBlips(1); + g1.gamepad.rumbleBlips(1); // 1 blip = lock released } + // X: re-zero the localizer at the current alliance's human player zone. if (g1.wasJustPressed(GamepadKeys.Button.X)) { + // Always relocalize regardless of which alliance is selected. if (colorGoalSelected.equals("Blue")) - aimer.relocalize(); + aimer.relocalize(); // re-zero for blue human player zone else if (colorGoalSelected.equals("Red")) - aimer.relocalize(); + aimer.relocalize(); // re-zero for red human player zone else { - aimer.relocalize(); + aimer.relocalize(); // default relocalize even if no alliance was explicitly set } } + // If lock is off, clear any lingering turn correction from the last active period. if (!continuousAprilTagLock){ - turnCorrection = 0; + turnCorrection = 0; // no heading correction when lock is disabled } } + // ----------------------------------------------------------------------- + // Utility + // ----------------------------------------------------------------------- + + /** + * Returns the current target RPM for the shooter flywheel. + * Subclasses use this to set the outtake RPM; it is exposed here so the + * Dashboard can override it and so range-based RPM updates propagate. + * + * @return the current shooter target RPM + */ protected double getTargetRPM() { - return targetRPM; + return targetRPM; // static field: shared across all instances and editable via Dashboard } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/MainTeleop.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/MainTeleop.java index 53376b8be969..11802bf98c5d 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/MainTeleop.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/MainTeleop.java @@ -8,19 +8,60 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; +/** + * MainTeleop is the two-driver teleop OpMode for competition matches. + * + *

This OpMode wires together the {@link Bot} controller with the FTC + * runtime in a simple three-phase pattern: + *

    + *
  1. Init – build the Road Runner {@link MecanumDrive}, construct + * {@link Bot}, and call {@link Bot#teleopInit()} to reset all subsystems.
  2. + *
  3. Start – call {@link Bot#teleopStart()} to position hardware + * for the start of the match once the driver presses PLAY.
  4. + *
  5. Loop – call {@link Bot#teleopTick()} every iteration until the + * match ends or the stop button is pressed.
  6. + *
+ * + *

Telemetry is multiplexed to both the Driver Station and FTC Dashboard via + * {@link MultipleTelemetry} so live data is visible on both displays during the + * match. + * + *

Drive mode is set to robot-centric single-driver + * ({@code twoMovement = false}): only gamepad 1 controls driving. + * For a two-movement (operator-also-drives) version see {@link OneDriverTeleop}. + * + *

Registered as {@code "MainTeleOp"} in the {@code "AA_main"} group so it + * appears at the top of the OpMode list on the Driver Station. + */ @TeleOp(name = "MainTeleOp", group = "AA_main") public class MainTeleop extends LinearOpMode { + /** + * Entry point called by the FTC runtime. + * Initialises hardware, waits for the start signal, then loops. + * + * @throws InterruptedException if the OpMode is interrupted while waiting + */ @Override public void runOpMode() throws InterruptedException { + // Mirror telemetry to FTC Dashboard so tuning values are visible on a laptop/PC. telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + // Construct the Road Runner MecanumDrive at the starting pose (0, 0, 0°). + // The localizer will be re-zeroed at the human player zone via the X button. MecanumDrive drive = new MecanumDrive(hardwareMap, new Pose2d(0, 0, Math.toRadians(0))); - Bot bot = new Bot(hardwareMap, telemetry, drive, gamepad1, gamepad2,false); - bot.teleopInit(); - waitForStart(); - bot.teleopStart(); + + // Build the Bot controller. false = gamepad 1 drives (standard two-driver mode). + Bot bot = new Bot(hardwareMap, telemetry, drive, gamepad1, gamepad2, false); + bot.teleopInit(); // reset subsystems during init phase + + waitForStart(); // block here until the driver presses the PLAY button + + bot.teleopStart(); // position hardware at match-start positions + + // Main teleop loop: run until the OpMode is stopped. while (opModeIsActive() && !isStopRequested()) { - bot.teleopTick(); + bot.teleopTick(); // run one complete control loop iteration } } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/OneDriverTeleop.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/OneDriverTeleop.java index a97e4661d643..cb3f5727f166 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/OneDriverTeleop.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/OneDriverTeleop.java @@ -8,19 +8,54 @@ import org.firstinspires.ftc.teamcode.auto.roadrunner.miscRR.MecanumDrive; +/** + * OneDriverTeleop is an alternative teleop OpMode intended for single-driver + * practice sessions or situations where only one gamepad is available. + * + *

The structure mirrors {@link MainTeleop} exactly; the only difference is + * the OpMode name registered with the FTC runtime ({@code "OneDriver"} instead + * of {@code "MainTeleOp"}). Both OpModes pass {@code twoMovement = false} to + * {@link Bot}, meaning only gamepad 1 controls driving. + * + *

This OpMode also appears in the {@code "AA_main"} group on the Driver + * Station for quick access. + * + *

Lifecycle: + *

    + *
  1. Init – construct {@link MecanumDrive} and {@link Bot}; + * call {@link Bot#teleopInit()}.
  2. + *
  3. Start – call {@link Bot#teleopStart()} once the match begins.
  4. + *
  5. Loop – call {@link Bot#teleopTick()} each iteration.
  6. + *
+ */ @TeleOp(name = "OneDriver", group = "AA_main") public class OneDriverTeleop extends LinearOpMode { + /** + * Entry point called by the FTC runtime. + * Initialises hardware, waits for the start signal, then loops. + * + * @throws InterruptedException if the OpMode is interrupted while waiting + */ @Override public void runOpMode() throws InterruptedException { + // Mirror telemetry to FTC Dashboard so live data is visible on a laptop. telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + // Create Road Runner drive at origin heading 0°. MecanumDrive drive = new MecanumDrive(hardwareMap, new Pose2d(0, 0, Math.toRadians(0))); - Bot bot = new Bot(hardwareMap, telemetry, drive, gamepad1, gamepad2,false); - bot.teleopInit(); - waitForStart(); - bot.teleopStart(); + + // Construct the Bot. false = use gamepad 1 for driving (single-driver mode). + Bot bot = new Bot(hardwareMap, telemetry, drive, gamepad1, gamepad2, false); + bot.teleopInit(); // reset all subsystems before the match starts + + waitForStart(); // pause here until the driver presses PLAY + + bot.teleopStart(); // position hardware for match start + + // Control loop: run every iteration until OpMode is stopped. while (opModeIsActive() && !isStopRequested()) { - bot.teleopTick(); + bot.teleopTick(); // execute one full control cycle } } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/TeleopConstants.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/TeleopConstants.java index bbd255525282..b095ce9b902d 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/TeleopConstants.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleop/TeleopConstants.java @@ -2,14 +2,55 @@ import com.acmerobotics.dashboard.config.Config; +/** + * TeleopConstants is a collection of compile-time (and Dashboard-editable) + * tuning constants used throughout the teleop OpModes. + * + *

Grouping constants here (rather than scattering magic numbers through + * the code) makes it easy to: + *

+ * + *

All fields are {@code public static} (not actually {@code final} at + * runtime) so that FTC Dashboard can write back updated values. The + * {@code final} modifier on the inner class is just syntactic sugar; the + * fields themselves are mutable through the Dashboard. + */ @Config public final class TeleopConstants { - //These variables use final notation but aren't actually final since they are editable in the dashboard + // Note: fields use final notation syntactically but are NOT truly final + // because FTC Dashboard can edit them at runtime through reflection. + + /** + * Constants that govern gamepad feedback (rumble, LEDs) and input + * filtering applied to both driver gamepads during teleop. + */ public static final class Gamepad { - public static int GAMEPAD_LIGHT_COLOR_DURATION = 500; + + /** + * Duration (milliseconds) for which the gamepad LED colour is held + * after an alliance-selection button press. After this time the LED + * reverts to its default colour. + */ + public static int GAMEPAD_LIGHT_COLOR_DURATION = 500; // ms + + /** + * Number of rumble "blips" sent to both gamepads when the indexer + * drum is completely full. Three short blips alert both drivers + * without being overly distracting. + */ public static int FULL_WARNING_RUMBLES = 3; - public static double TRIGGER_DEADZONE = 0.05; + + /** + * Minimum trigger axis value that counts as a deliberate button press. + * Values below this threshold are treated as 0. Prevents unintended + * intake activation from small amounts of resting-finger pressure on + * the trigger buttons. + */ + public static double TRIGGER_DEADZONE = 0.05; // trigger value below this is ignored } // }