From edd589fb79f95634b3bed68ff88cdbee909381c0 Mon Sep 17 00:00:00 2001 From: UntitledRice Date: Thu, 26 Oct 2023 19:09:45 -0500 Subject: [PATCH] Add drive base, add intake, and other small details. --- TeamCode/build.gradle | 2 + TeamCode/src/main/java/opmodes/Sandbox.java | 28 - .../ftc/teamcode/controller/Button.java | 30 + .../ftc/teamcode/controller/Controller.java | 197 ++++ .../ftc/teamcode/controller/Gamepad2.java | 913 ++++++++++++++++++ .../ftc/teamcode/controller/Joystick.java | 32 + .../ftc/teamcode/controller/Trigger.java | 33 + .../ftc/teamcode/hardware/Camera.java | 71 +- .../ftc/teamcode/hardware/Drive.java | 129 ++- .../ftc/teamcode/hardware/Intake.java | 74 ++ .../ftc/teamcode/hardware/Robot.java | 33 +- .../opmode/autonomous/AbstractAuto.java | 50 + .../autonomous/StartFromLeftCenterSpike.java | 65 ++ .../opmode/teleop/AbstractTeleOp.java | 150 +++ .../ftc/teamcode/opmode/teleop/KhangMain.java | 71 ++ .../roadrunner/drive/DriveConstants.java | 89 ++ .../roadrunner/drive/MouseOdometry.java | 5 + .../roadrunner/drive/SampleMecanumDrive.java | 326 +++++++ .../roadrunner/drive/SampleTankDrive.java | 321 ++++++ .../drive/StandardTrackingWheelLocalizer.java | 82 ++ .../drive/TwoWheelTrackingLocalizer.java | 108 +++ .../opmode/AutomaticFeedforwardTuner.java | 203 ++++ .../roadrunner/drive/opmode/BackAndForth.java | 51 + .../drive/opmode/DriveVelocityPIDTuner.java | 171 ++++ .../drive/opmode/FollowerPIDTuner.java | 56 ++ .../drive/opmode/LocalizationTest.java | 45 + .../drive/opmode/ManualFeedforwardTuner.java | 143 +++ .../drive/opmode/MaxAngularVeloTuner.java | 71 ++ .../drive/opmode/MaxVelocityTuner.java | 83 ++ .../drive/opmode/MotorDirectionDebugger.java | 90 ++ .../roadrunner/drive/opmode/SplineTest.java | 40 + .../roadrunner/drive/opmode/StrafeTest.java | 46 + .../roadrunner/drive/opmode/StraightTest.java | 44 + .../drive/opmode/TrackWidthTuner.java | 88 ++ .../TrackingWheelForwardOffsetTuner.java | 104 ++ .../TrackingWheelLateralDistanceTuner.java | 131 +++ .../roadrunner/drive/opmode/TurnTest.java | 28 + .../EmptySequenceException.java | 4 + .../TrajectorySequence.java | 44 + .../TrajectorySequenceBuilder.java | 711 ++++++++++++++ .../TrajectorySequenceRunner.java | 271 ++++++ .../sequencesegment/SequenceSegment.java | 40 + .../sequencesegment/TrajectorySegment.java | 20 + .../sequencesegment/TurnSegment.java | 36 + .../sequencesegment/WaitSegment.java | 12 + .../util/AssetsTrajectoryManager.java | 70 ++ .../teamcode/roadrunner/util/AxesSigns.java | 45 + .../roadrunner/util/AxisDirection.java | 8 + .../roadrunner/util/BNO055IMUUtil.java | 128 +++ .../roadrunner/util/DashboardUtil.java | 54 ++ .../ftc/teamcode/roadrunner/util/Encoder.java | 98 ++ .../teamcode/roadrunner/util/LoggingUtil.java | 60 ++ .../roadrunner/util/LynxModuleUtil.java | 124 +++ .../ftc/teamcode/util/CameraPosition.java | 5 + .../ftc/teamcode/util/Configurables.java | 2 + .../vision/AprilTagDetectionPipeline.java | 194 ++++ .../ftc/teamcode/vision/OpenCVUtil.java | 109 +++ .../teamcode/vision/TargetingPipeline.java | 76 +- build.dependencies.gradle | 2 + 59 files changed, 6250 insertions(+), 66 deletions(-) delete mode 100644 TeamCode/src/main/java/opmodes/Sandbox.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Button.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Controller.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Gamepad2.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Joystick.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Trigger.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Intake.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/AbstractAuto.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/StartFromLeftCenterSpike.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/AbstractTeleOp.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/KhangMain.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/DriveConstants.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/MouseOdometry.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleMecanumDrive.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleTankDrive.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/StandardTrackingWheelLocalizer.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/TwoWheelTrackingLocalizer.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/AutomaticFeedforwardTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/BackAndForth.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/DriveVelocityPIDTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/FollowerPIDTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/LocalizationTest.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/ManualFeedforwardTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxAngularVeloTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxVelocityTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MotorDirectionDebugger.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/SplineTest.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StrafeTest.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StraightTest.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackWidthTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelForwardOffsetTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelLateralDistanceTuner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TurnTest.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/EmptySequenceException.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequence.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceBuilder.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceRunner.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/SequenceSegment.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TrajectorySegment.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TurnSegment.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/WaitSegment.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AssetsTrajectoryManager.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxesSigns.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxisDirection.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/BNO055IMUUtil.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/DashboardUtil.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/Encoder.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LoggingUtil.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LynxModuleUtil.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/CameraPosition.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/AprilTagDetectionPipeline.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/OpenCVUtil.java diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 436ce67..c5a0398 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -26,4 +26,6 @@ android { dependencies { implementation project(':FtcRobotController') annotationProcessor files('lib/OpModeAnnotationProcessor.jar') + implementation 'org.openftc:easyopencv:1.7.0' + implementation 'com.acmerobotics.roadrunner:core:0.5.5' } diff --git a/TeamCode/src/main/java/opmodes/Sandbox.java b/TeamCode/src/main/java/opmodes/Sandbox.java deleted file mode 100644 index 755a6a1..0000000 --- a/TeamCode/src/main/java/opmodes/Sandbox.java +++ /dev/null @@ -1,28 +0,0 @@ -package opmodes; - -import com.qualcomm.robotcore.eventloop.opmode.OpMode; -import com.qualcomm.robotcore.eventloop.opmode.TeleOp; - -import org.firstinspires.ftc.teamcode.hardware.Camera; -import org.firstinspires.ftc.teamcode.hardware.Robot; - -/* - * Demonstrates an empty iterative OpMode - */ -@TeleOp(name = "Sandbox", group = "Experimental") -public class Sandbox extends OpMode { - - private Camera camera; - - @Override - public void init() { - telemetry.addData("Status", "Initialized"); - this.camera = new Camera(this.hardwareMap); - this.camera.initTargetingCamera(); - } - - @Override - public void loop() { - - } -} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Button.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Button.java new file mode 100644 index 0000000..876a3af --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Button.java @@ -0,0 +1,30 @@ +package org.firstinspires.ftc.teamcode.controller; + +public class Button { + boolean currentState = false; + boolean lastState = false; + boolean justPressed = false; + boolean justReleased = false; + + public Button() {} + + public void update(boolean pressed) { + lastState = currentState; + currentState = pressed; + + justPressed = currentState && !lastState; + justReleased = !currentState && lastState; + } + + public boolean isPressed() { + return currentState; + } + + public boolean isJustPressed() { + return justPressed; + } + + public boolean isJustReleased() { + return justReleased; + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Controller.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Controller.java new file mode 100644 index 0000000..75e6dea --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Controller.java @@ -0,0 +1,197 @@ +package org.firstinspires.ftc.teamcode.controller; + +import com.qualcomm.robotcore.hardware.Gamepad; + +public class Controller { + private final Gamepad gamepad; + + private final Joystick leftStick; + private final Joystick rightStick; + + private final Button leftStickButton; + private final Button rightStickButton; + + private final Button dLeft; + private final Button dRight; + private final Button dUp; + private final Button dDown; + + private final Button a; + private final Button b; + private final Button x; + private final Button y; + + private final Button leftBumper; + private final Button rightBumper; + + private final Button back; + private final Button start; + + private final Trigger leftTrigger; + private final Trigger rightTrigger; + + private final Button touchpad; + + private final Button allButtons; + + public Controller(Gamepad gamepad) { + this.gamepad = gamepad; + + leftStick = new Joystick(); + rightStick = new Joystick(); + + leftStickButton = new Button(); + rightStickButton = new Button(); + + dLeft = new Button(); + dRight = new Button(); + dUp = new Button(); + dDown = new Button(); + + a = new Button(); + b = new Button(); + x = new Button(); + y = new Button(); + + leftBumper = new Button(); + rightBumper = new Button(); + + back = new Button(); + start = new Button(); + + leftTrigger = new Trigger(); + rightTrigger = new Trigger(); + + touchpad = new Button(); + + allButtons = new Button(); + } + + + public void update() { + leftStick.update(gamepad.left_stick_x, -gamepad.left_stick_y); + rightStick.update(gamepad.right_stick_x, -gamepad.right_stick_y); + + leftStickButton.update(gamepad.left_stick_button); + rightStickButton.update(gamepad.right_stick_button); + + dLeft.update(gamepad.dpad_left); + dRight.update(gamepad.dpad_right); + dUp.update(gamepad.dpad_up); + dDown.update(gamepad.dpad_down); + + a.update(gamepad.a); + b.update(gamepad.b); + x.update(gamepad.x); + y.update(gamepad.y); + + leftBumper.update(gamepad.left_bumper); + rightBumper.update(gamepad.right_bumper); + + back.update(gamepad.back); + start.update(gamepad.start); + + leftTrigger.update(gamepad.left_trigger); + rightTrigger.update(gamepad.right_trigger); + + touchpad.update(gamepad.touchpad); + + allButtons.update(leftStickButton.isPressed() || rightStickButton.isPressed() || dLeft.isPressed() || dRight.isPressed() || dUp.isPressed() || dDown.isPressed() || a.isPressed() || b.isPressed() || x.isPressed() || y.isPressed() || leftBumper.isPressed() || rightBumper.isPressed() || back.isPressed() || start.isPressed() || leftTrigger.getValue() > 0 || rightTrigger.getValue() > 0 || touchpad.isPressed()); + } + + public Joystick getLeftStick() { + return leftStick; + } + public Joystick getRightStick() { + return rightStick; + } + + public Button getLeftStickButton() { + return leftStickButton; + } + public Button getRightStickButton() { + return rightStickButton; + } + + public Button getDLeft() { + return dLeft; + } + public Button getDRight() { + return dRight; + } + public Button getDUp() { + return dUp; + } + public Button getDDown() { + return dDown; + } + + public Button getA() { + return a; + } + public Button getB() { + return b; + } + public Button getX() { + return x; + } + public Button getY() { + return y; + } + + public Button getLeftBumper() { + return leftBumper; + } + public Button getRightBumper() { + return rightBumper; + } + + public Button getBack() { + return back; + } + public Button getStart() { + return start; + } + + public Trigger getLeftTrigger() { + return leftTrigger; + } + public Trigger getRightTrigger() { + return rightTrigger; + } + + public Button getTouchpad() { + return touchpad; + } + + public Button getAllButtons() { + return allButtons; + } + + public void rumble() { + gamepad.rumble(Gamepad.RUMBLE_DURATION_CONTINUOUS); + } + + public void rumble(int milliseconds) { + gamepad.rumble(milliseconds); + } + + public void rumbleBlips(int blips) { + gamepad.rumbleBlips(blips); + } + + public void stopRumble() { + gamepad.stopRumble(); + } + + public boolean isRumbling() { + return gamepad.isRumbling(); + } + + public void setColor(int r, int g, int b) { + gamepad.setLedColor(r, g, b, -1); + } + public void setColor(int r, int g, int b, int milliseconds) { + gamepad.setLedColor(r, g, b, milliseconds); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Gamepad2.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Gamepad2.java new file mode 100644 index 0000000..201a84b --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Gamepad2.java @@ -0,0 +1,913 @@ +package org.firstinspires.ftc.teamcode.controller;///* +// * Copyright (c) 2014, 2015 Qualcomm Technologies Inc +// * +// * All rights reserved. +// * +// * Redistribution and use in source and binary forms, with or without modification, are permitted +// * (subject to the limitations in the disclaimer below) provided that the following conditions are +// * met: +// * +// * Redistributions of source code must retain the above copyright notice, this list of conditions +// * and the following disclaimer. +// * +// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions +// * and the following disclaimer in the documentation and/or other materials provided with the +// * distribution. +// * +// * Neither the name of Qualcomm Technologies Inc nor the names of its contributors may be used to +// * endorse or promote products derived from this software without specific prior written permission. +// * +// * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS +// * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +// * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +// * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// */ +// +//package com.qualcomm.robotcore.hardware; +// +//import android.os.SystemClock; +// +//import com.qualcomm.robotcore.eventloop.opmode.OpModeManagerImpl; +//import com.qualcomm.robotcore.exception.RobotCoreException; +//import com.qualcomm.robotcore.robocol.RobocolParsable; +//import com.qualcomm.robotcore.robocol.RobocolParsableBase; +//import com.qualcomm.robotcore.util.Range; +//import com.qualcomm.robotcore.util.RobotLog; +// +//import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; +//import org.firstinspires.ftc.robotcore.internal.collections.SimpleGson; +//import org.firstinspires.ftc.robotcore.internal.network.SendOnceRunnable; +//import org.firstinspires.ftc.robotcore.internal.ui.GamepadUser; +// +//import java.nio.BufferOverflowException; +//import java.nio.ByteBuffer; +//import java.util.ArrayList; +//import java.util.concurrent.ArrayBlockingQueue; +// +///** +// * Monitor a hardware gamepad. +// *

+// * The buttons, analog sticks, and triggers are represented a public +// * member variables that can be read from or written to directly. +// *

+// * Analog sticks are represented as floats that range from -1.0 to +1.0. They will be 0.0 while at +// * rest. The horizontal axis is labeled x, and the vertical axis is labeled y. +// *

+// * Triggers are represented as floats that range from 0.0 to 1.0. They will be at 0.0 while at +// * rest. +// *

+// * Buttons are boolean values. They will be true if the button is pressed, otherwise they will be +// * false. +// *

+// * The codes KEYCODE_BUTTON_SELECT and KEYCODE_BACK are both be handled as a "back" button event. +// * Older Android devices (Kit Kat) map a Logitech F310 "back" button press to a KEYCODE_BUTTON_SELECT event. +// * Newer Android devices (Marshmallow or greater) map this "back" button press to a KEYCODE_BACK event. +// * Also, the REV Robotics Gamepad (REV-31-1159) has a "select" button instead of a "back" button on the gamepad. +// *

+// * The dpad is represented as 4 buttons, dpad_up, dpad_down, dpad_left, and dpad_right +// */ +//@SuppressWarnings("unused") +//public class Gamepad2 extends RobocolParsableBase { +// +// /** +// * A gamepad with an ID equal to ID_UNASSOCIATED has not been associated with any device. +// */ +// public static final int ID_UNASSOCIATED = -1; +// +// /** +// * A gamepad with a phantom id a synthetic one made up by the system +// */ +// public static final int ID_SYNTHETIC = -2; +// +// public enum Type { +// // Do NOT change the order/names of existing entries, +// // you will break backwards compatibility!! +// UNKNOWN(LegacyType.UNKNOWN), +// LOGITECH_F310(LegacyType.LOGITECH_F310), +// XBOX_360(LegacyType.XBOX_360), +// SONY_PS4(LegacyType.SONY_PS4), // This indicates a PS4-compatible controller that is being used through our compatibility mode +// SONY_PS4_SUPPORTED_BY_KERNEL(LegacyType.SONY_PS4); // This indicates a PS4-compatible controller that is being used through the DualShock 4 Linux kernel driver. +// +// private final LegacyType correspondingLegacyType; +// Type(LegacyType correspondingLegacyType) { +// this.correspondingLegacyType = correspondingLegacyType; +// } +// } +// +// // LegacyType is necessary because robocol gamepad version 3 was written in a way that was not +// // forwards-compatible, so we have to keep sending V3-compatible values. +// public enum LegacyType { +// // Do NOT change the order or names of existing entries, or add new entries. +// // You will break backwards compatibility!! +// UNKNOWN, +// LOGITECH_F310, +// XBOX_360, +// SONY_PS4; +// } +// +// @SuppressWarnings("UnusedAssignment") +// public volatile Type type = Type.UNKNOWN; // IntelliJ thinks this is redundant, but it is NOT. Must be a bug in the analyzer? +// +// /** +// * left analog stick horizontal axis +// */ +// public volatile float left_stick_x = 0f; +// +// /** +// * left analog stick vertical axis +// */ +// public volatile float left_stick_y = 0f; +// +// /** +// * right analog stick horizontal axis +// */ +// public volatile float right_stick_x = 0f; +// +// /** +// * right analog stick vertical axis +// */ +// public volatile float right_stick_y = 0f; +// +// /** +// * dpad up +// */ +// public volatile boolean dpad_up = false; +// +// /** +// * dpad down +// */ +// public volatile boolean dpad_down = false; +// +// /** +// * dpad left +// */ +// public volatile boolean dpad_left = false; +// +// /** +// * dpad right +// */ +// public volatile boolean dpad_right = false; +// +// /** +// * button a +// */ +// public volatile boolean a = false; +// +// /** +// * button b +// */ +// public volatile boolean b = false; +// +// /** +// * button x +// */ +// public volatile boolean x = false; +// +// /** +// * button y +// */ +// public volatile boolean y = false; +// +// /** +// * button guide - often the large button in the middle of the controller. The OS may +// * capture this button before it is sent to the app; in which case you'll never +// * receive it. +// */ +// public volatile boolean guide = false; +// +// /** +// * button start +// */ +// public volatile boolean start = false; +// +// /** +// * button back +// */ +// public volatile boolean back = false; +// +// /** +// * button left bumper +// */ +// public volatile boolean left_bumper = false; +// +// /** +// * button right bumper +// */ +// public volatile boolean right_bumper = false; +// +// /** +// * left stick button +// */ +// public volatile boolean left_stick_button = false; +// +// /** +// * right stick button +// */ +// public volatile boolean right_stick_button = false; +// +// /** +// * left trigger +// */ +// public volatile float left_trigger = 0f; +// +// /** +// * right trigger +// */ +// public volatile float right_trigger = 0f; +// +// /** +// * PS4 Support - Circle +// */ +// public volatile boolean circle = false; +// +// /** +// * PS4 Support - cross +// */ +// public volatile boolean cross = false; +// +// /** +// * PS4 Support - triangle +// */ +// public volatile boolean triangle = false; +// +// /** +// * PS4 Support - square +// */ +// public volatile boolean square = false; +// +// /** +// * PS4 Support - share +// */ +// public volatile boolean share = false; +// +// /** +// * PS4 Support - options +// */ +// public volatile boolean options = false; +// +// /** +// * PS4 Support - touchpad +// */ +// public volatile boolean touchpad = false; +// public volatile boolean touchpad_finger_1; +// public volatile boolean touchpad_finger_2; +// public volatile float touchpad_finger_1_x; +// public volatile float touchpad_finger_1_y; +// public volatile float touchpad_finger_2_x; +// public volatile float touchpad_finger_2_y; +// +// /** +// * PS4 Support - PS Button +// */ +// public volatile boolean ps = false; +// +// /** +// * Which user is this gamepad used by +// */ +// protected volatile byte user = ID_UNASSOCIATED; +// // +// public GamepadUser getUser() { +// return GamepadUser.from(user); +// } +// // +// public void setUser(GamepadUser user) { +// this.user = user.id; +// } +// +// /** +// * See {@link OpModeManagerImpl#runActiveOpMode(Gamepad[])} +// */ +// protected volatile byte userForEffects = ID_UNASSOCIATED; +// public void setUserForEffects(byte userForEffects) { +// this.userForEffects = userForEffects; +// } +// +// /** +// * ID assigned to this gamepad by the OS. This value can change each time the device is plugged in. +// */ +// public volatile int id = ID_UNASSOCIATED; // public only for historical reasons +// +// public void setGamepadId(int id) { +// this.id = id; +// } +// public int getGamepadId() { +// return this.id; +// } +// +// /** +// * Relative timestamp of the last time an event was detected +// */ +// public volatile long timestamp = 0; +// +// /** +// * Sets the time at which this Gamepad last changed its state, +// * in the {@link android.os.SystemClock#uptimeMillis} time base. +// */ +// public void setTimestamp(long timestamp) { +// this.timestamp = timestamp; +// } +// +// /** +// * Refreshes the Gamepad's timestamp to be the current time. +// */ +// public void refreshTimestamp() { +// setTimestamp(SystemClock.uptimeMillis()); +// } +// +// // private static values used for packaging the gamepad state into a byte array +// private static final short PAYLOAD_SIZE = 60; +// private static final short BUFFER_SIZE = PAYLOAD_SIZE + RobocolParsable.HEADER_LENGTH; +// +// private static final byte ROBOCOL_GAMEPAD_VERSION = 5; +// +// public Gamepad() { +// this.type = type(); +// } +// +// /** +// * Copy the state of a gamepad into this gamepad +// * @param gamepad state to be copied from +// * @throws RobotCoreException if the copy fails - gamepad will be in an unknown +// * state if this exception is thrown +// */ +// public void copy(Gamepad gamepad) throws RobotCoreException { +// // reuse the serialization code; since that reduces the chances of bugs +// fromByteArray(gamepad.toByteArray()); +// } +// +// /** +// * Reset this gamepad into its initial state +// */ +// public void reset() { +// try { +// copy(new Gamepad()); +// } catch (RobotCoreException e) { +// // we should never hit this +// RobotLog.e("Gamepad library in an invalid state"); +// throw new IllegalStateException("Gamepad library in an invalid state"); +// } +// } +// +// @Override +// public MsgType getRobocolMsgType() { +// return RobocolParsable.MsgType.GAMEPAD; +// } +// +// @Override +// public byte[] toByteArray() throws RobotCoreException { +// +// ByteBuffer buffer = getWriteBuffer(PAYLOAD_SIZE); +// +// try { +// int buttons = 0; +// +// buffer.put(ROBOCOL_GAMEPAD_VERSION); +// buffer.putInt(id); +// buffer.putLong(timestamp).array(); +// buffer.putFloat(left_stick_x).array(); +// buffer.putFloat(left_stick_y).array(); +// buffer.putFloat(right_stick_x).array(); +// buffer.putFloat(right_stick_y).array(); +// buffer.putFloat(left_trigger).array(); +// buffer.putFloat(right_trigger).array(); +// +// buttons = (buttons << 1) + (touchpad_finger_1 ? 1 : 0); +// buttons = (buttons << 1) + (touchpad_finger_2 ? 1 : 0); +// buttons = (buttons << 1) + (touchpad ? 1 : 0); +// buttons = (buttons << 1) + (left_stick_button ? 1 : 0); +// buttons = (buttons << 1) + (right_stick_button ? 1 : 0); +// buttons = (buttons << 1) + (dpad_up ? 1 : 0); +// buttons = (buttons << 1) + (dpad_down ? 1 : 0); +// buttons = (buttons << 1) + (dpad_left ? 1 : 0); +// buttons = (buttons << 1) + (dpad_right ? 1 : 0); +// buttons = (buttons << 1) + (a ? 1 : 0); +// buttons = (buttons << 1) + (b ? 1 : 0); +// buttons = (buttons << 1) + (x ? 1 : 0); +// buttons = (buttons << 1) + (y ? 1 : 0); +// buttons = (buttons << 1) + (guide ? 1 : 0); +// buttons = (buttons << 1) + (start ? 1 : 0); +// buttons = (buttons << 1) + (back ? 1 : 0); +// buttons = (buttons << 1) + (left_bumper ? 1 : 0); +// buttons = (buttons << 1) + (right_bumper ? 1 : 0); +// buffer.putInt(buttons); +// +// // Version 2 +// buffer.put(user); +// +// // Version 3 +// buffer.put((byte) legacyType().ordinal()); +// +// // Version 4 +// buffer.put((byte) type.ordinal()); +// +// // Version 5 +// buffer.putFloat(touchpad_finger_1_x); +// buffer.putFloat(touchpad_finger_1_y); +// buffer.putFloat(touchpad_finger_2_x); +// buffer.putFloat(touchpad_finger_2_y); +// } catch (BufferOverflowException e) { +// RobotLog.logStacktrace(e); +// } +// +// return buffer.array(); +// } +// +// @Override +// public void fromByteArray(byte[] byteArray) throws RobotCoreException { +// if (byteArray.length < BUFFER_SIZE) { +// throw new RobotCoreException("Expected buffer of at least " + BUFFER_SIZE + " bytes, received " + byteArray.length); +// } +// +// ByteBuffer byteBuffer = getReadBuffer(byteArray); +// +// int buttons = 0; +// +// byte version = byteBuffer.get(); +// +// // TODO(Noah): Reset version to 1 +// // extract version 1 values +// if (version >= 1) { +// id = byteBuffer.getInt(); +// timestamp = byteBuffer.getLong(); +// left_stick_x = byteBuffer.getFloat(); +// left_stick_y = byteBuffer.getFloat(); +// right_stick_x = byteBuffer.getFloat(); +// right_stick_y = byteBuffer.getFloat(); +// left_trigger = byteBuffer.getFloat(); +// right_trigger = byteBuffer.getFloat(); +// +// buttons = byteBuffer.getInt(); +// touchpad_finger_1 = (buttons & 0x20000) != 0; +// touchpad_finger_2 = (buttons & 0x10000) != 0; +// touchpad = (buttons & 0x08000) != 0; +// left_stick_button = (buttons & 0x04000) != 0; +// right_stick_button = (buttons & 0x02000) != 0; +// dpad_up = (buttons & 0x01000) != 0; +// dpad_down = (buttons & 0x00800) != 0; +// dpad_left = (buttons & 0x00400) != 0; +// dpad_right = (buttons & 0x00200) != 0; +// a = (buttons & 0x00100) != 0; +// b = (buttons & 0x00080) != 0; +// x = (buttons & 0x00040) != 0; +// y = (buttons & 0x00020) != 0; +// guide = (buttons & 0x00010) != 0; +// start = (buttons & 0x00008) != 0; +// back = (buttons & 0x00004) != 0; +// left_bumper = (buttons & 0x00002) != 0; +// right_bumper = (buttons & 0x00001) != 0; +// } +// +// // extract version 2 values +// if (version >= 2) { +// user = byteBuffer.get(); +// } +// +// // extract version 3 values +// if (version >= 3) { +// type = Type.values()[byteBuffer.get()]; +// } +// +// if (version >= 4) { +// byte v4TypeValue = byteBuffer.get(); +// if (v4TypeValue < Type.values().length) { +// // Yes, this will replace the version 3 value. That is a good thing, since the version 3 +// // value was not forwards-compatible. +// type = Type.values()[v4TypeValue]; +// } // Else, we don't know what the number means, so we just stick with the value we got from the v3 type field +// } +// +// if(version >= 5) { +// touchpad_finger_1_x = byteBuffer.getFloat(); +// touchpad_finger_1_y = byteBuffer.getFloat(); +// touchpad_finger_2_x = byteBuffer.getFloat(); +// touchpad_finger_2_y = byteBuffer.getFloat(); +// } +// +// updateButtonAliases(); +// } +// +// /** +// * Are all analog sticks and triggers in their rest position? +// * @return true if all analog sticks and triggers are at rest; otherwise false +// */ +// public boolean atRest() { +// return ( +// left_stick_x == 0f && left_stick_y == 0f && +// right_stick_x == 0f && right_stick_y == 0f && +// left_trigger == 0f && right_trigger == 0f); +// } +// +// /** +// * Get the type of gamepad as a {@link Type}. This method defaults to "UNKNOWN". +// * @return gamepad type +// */ +// public Type type() { +// return type; +// } +// +// /** +// * Get the type of gamepad as a {@link LegacyType}. This method defaults to "UNKNOWN". +// * @return gamepad type +// */ +// private LegacyType legacyType() { +// return type.correspondingLegacyType; +// } +// +// +// /** +// * Display a summary of this gamepad, including the state of all buttons, analog sticks, and triggers +// * @return a summary +// */ +// @Override +// public String toString() { +// +// switch (type) { +// case SONY_PS4: +// case SONY_PS4_SUPPORTED_BY_KERNEL: +// return ps4ToString(); +// +// case UNKNOWN: +// case LOGITECH_F310: +// case XBOX_360: +// default: +// return genericToString(); +// } +// } +// +// +// protected String ps4ToString() { +// String buttons = new String(); +// if (dpad_up) buttons += "dpad_up "; +// if (dpad_down) buttons += "dpad_down "; +// if (dpad_left) buttons += "dpad_left "; +// if (dpad_right) buttons += "dpad_right "; +// if (cross) buttons += "cross "; +// if (circle) buttons += "circle "; +// if (square) buttons += "square "; +// if (triangle) buttons += "triangle "; +// if (ps) buttons += "ps "; +// if (share) buttons += "share "; +// if (options) buttons += "options "; +// if (touchpad) buttons += "touchpad "; +// if (left_bumper) buttons += "left_bumper "; +// if (right_bumper) buttons += "right_bumper "; +// if (left_stick_button) buttons += "left stick button "; +// if (right_stick_button) buttons += "right stick button "; +// +// return String.format("ID: %2d user: %2d lx: % 1.2f ly: % 1.2f rx: % 1.2f ry: % 1.2f lt: %1.2f rt: %1.2f %s", +// id, user, left_stick_x, left_stick_y, +// right_stick_x, right_stick_y, left_trigger, right_trigger, buttons); +// } +// +// protected String genericToString() { +// String buttons = new String(); +// if (dpad_up) buttons += "dpad_up "; +// if (dpad_down) buttons += "dpad_down "; +// if (dpad_left) buttons += "dpad_left "; +// if (dpad_right) buttons += "dpad_right "; +// if (a) buttons += "a "; +// if (b) buttons += "b "; +// if (x) buttons += "x "; +// if (y) buttons += "y "; +// if (guide) buttons += "guide "; +// if (start) buttons += "start "; +// if (back) buttons += "back "; +// if (left_bumper) buttons += "left_bumper "; +// if (right_bumper) buttons += "right_bumper "; +// if (left_stick_button) buttons += "left stick button "; +// if (right_stick_button) buttons += "right stick button "; +// +// return String.format("ID: %2d user: %2d lx: % 1.2f ly: % 1.2f rx: % 1.2f ry: % 1.2f lt: %1.2f rt: %1.2f %s", +// id, user, left_stick_x, left_stick_y, +// right_stick_x, right_stick_y, left_trigger, right_trigger, buttons); +// } +// +// +// // To prevent blowing up the command queue if the user tries to send an LED command in a tight loop, +// // we have a 1-element evicting blocking queue for the outgoing LED effect and the event loop periodically +// // just grabs the effect out of it (if any) +// public EvictingBlockingQueue ledQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue(1)); +// +// public static final int LED_DURATION_CONTINUOUS = -1; +// +// public static class LedEffect { +// public static class Step { +// public int r; +// public int g; +// public int b; +// public int duration; +// } +// +// public final ArrayList steps; +// public final boolean repeating; +// public int user; +// +// private LedEffect(ArrayList steps, boolean repeating) { +// this.steps = steps; +// this.repeating = repeating; +// } +// +// public String serialize() { +// return SimpleGson.getInstance().toJson(this); +// } +// +// public static LedEffect deserialize(String serialized) { +// return SimpleGson.getInstance().fromJson(serialized, LedEffect.class); +// } +// +// public static class Builder { +// private ArrayList steps = new ArrayList<>(); +// private boolean repeating; +// +// /** +// * Add a "step" to this LED effect. A step basically just means to set +// * the LED to a certain color (r,g,b) for a certain duration. By creating a chain of +// * steps, you can create unique effects. +// * +// * @param r the red LED intensity (0.0 - 1.0) +// * @param g the green LED intensity (0.0 - 1.0) +// * @param b the blue LED intensity (0.0 - 1.0) +// * @return the builder object, to follow the builder pattern +// */ +// public Builder addStep(double r, double g, double b, int durationMs) { +// return addStepInternal(r, g, b, Math.max(durationMs, 0)); +// } +// +// /** +// * Set whether this LED effect should loop after finishing, +// * unless the LED is otherwise commanded differently. +// * @param repeating whether to loop +// * @return the builder object, to follow the builder pattern +// */ +// public Builder setRepeating(boolean repeating) { +// this.repeating = repeating; +// return this; +// } +// +// private Builder addStepInternal(double r, double g, double b, int duration) { +// +// r = Range.clip(r, 0, 1); +// g = Range.clip(g, 0, 1); +// b = Range.clip(b, 0, 1); +// +// Step step = new Step(); +// step.r = (int) Math.round(Range.scale(r, 0.0, 1.0, 0, 255)); +// step.g = (int) Math.round(Range.scale(g, 0.0, 1.0, 0, 255)); +// step.b = (int) Math.round(Range.scale(b, 0.0, 1.0, 0, 255)); +// step.duration = duration; +// steps.add(step); +// return this; +// } +// +// /** +// * After you've added your steps, call this to get an LedEffect object +// * that you can then pass to {@link #runLedEffect(LedEffect)} +// * @return an LedEffect object, built from previously added steps +// */ +// public LedEffect build() { +// return new LedEffect(steps, repeating); +// } +// } +// } +// +// public void setLedColor(double r, double g, double b, int durationMs) { +// +// if (durationMs != LED_DURATION_CONTINUOUS) { +// durationMs = Math.max(0, durationMs); +// } +// +// LedEffect effect = new LedEffect.Builder().addStepInternal(r,g,b, durationMs).build(); +// queueEffect(effect); +// } +// +// /** +// * Run an LED effect built using {@link LedEffect.Builder} +// * The LED effect will be run asynchronously; your OpMode will +// * not halt execution while the effect is running. +// * +// * Calling this will displace any currently running LED effect +// */ +// public void runLedEffect(LedEffect effect) { +// queueEffect(effect); +// } +// +// private void queueEffect(LedEffect effect) { +// // We need to make a new object here, because since the effect is queued and +// // not set immediately, if you called this function for two different gamepads +// // but passed in the same instance of an LED effect object, then it could happen +// // that both effect commands would be directed to the *same* gamepad. (Because of +// // the "user" field being modified before the queued command was serialized) +// LedEffect copy = new LedEffect(effect.steps, effect.repeating); +// copy.user = userForEffects; +// ledQueue.offer(copy); +// } +// +// // To prevent blowing up the command queue if the user tries to send a rumble command in a tight loop, +// // we have a 1-element evicting blocking queue for the outgoing rumble effect and the event loop periodically +// // just grabs the effect out of it (if any) +// public EvictingBlockingQueue rumbleQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue(1)); +// public long nextRumbleApproxFinishTime = RUMBLE_FINISH_TIME_FLAG_NOT_RUMBLING; +// +// public static final int RUMBLE_DURATION_CONTINUOUS = -1; +// +// public static class RumbleEffect { +// public static class Step { +// public int large; +// public int small; +// public int duration; +// } +// +// public int user; +// public final ArrayList steps; +// +// private RumbleEffect(ArrayList steps) { +// this.steps = steps; +// } +// +// public String serialize() { +// return SimpleGson.getInstance().toJson(this); +// } +// +// public static RumbleEffect deserialize(String serialized) { +// return SimpleGson.getInstance().fromJson(serialized, RumbleEffect.class); +// } +// +// public static class Builder { +// private ArrayList steps = new ArrayList<>(); +// +// /** +// * Add a "step" to this rumble effect. A step basically just means to rumble +// * at a certain power level for a certain duration. By creating a chain of +// * steps, you can create unique effects. See {@link #rumbleBlips(int)} for a +// * a simple example. +// * @param rumble1 rumble power for rumble motor 1 (0.0 - 1.0) +// * @param rumble2 rumble power for rumble motor 2 (0.0 - 1.0) +// * @param durationMs milliseconds this step lasts +// * @return the builder object, to follow the builder pattern +// */ +// public Builder addStep(double rumble1, double rumble2, int durationMs) { +// return addStepInternal(rumble1, rumble2, Math.max(durationMs, 0)); +// } +// +// private Builder addStepInternal(double rumble1, double rumble2, int durationMs) { +// +// rumble1 = Range.clip(rumble1, 0, 1); +// rumble2 = Range.clip(rumble2, 0, 1); +// +// Step step = new Step(); +// step.large = (int) Math.round(Range.scale(rumble1, 0.0, 1.0, 0, 255)); +// step.small = (int) Math.round(Range.scale(rumble2, 0.0, 1.0, 0, 255)); +// step.duration = durationMs; +// steps.add(step); +// +// return this; +// } +// +// /** +// * After you've added your steps, call this to get a RumbleEffect object +// * that you can then pass to {@link #runRumbleEffect(RumbleEffect)} +// * @return a RumbleEffect object, built from previously added steps +// */ +// public RumbleEffect build() { +// return new RumbleEffect(steps); +// } +// } +// } +// +// /** +// * Run a rumble effect built using {@link RumbleEffect.Builder} +// * The rumble effect will be run asynchronously; your OpMode will +// * not halt execution while the effect is running. +// * +// * Calling this will displace any currently running rumble effect +// */ +// public void runRumbleEffect(RumbleEffect effect) { +// queueEffect(effect); +// } +// +// /** +// * Rumble the gamepad's first rumble motor at maximum power for a certain duration. +// * Calling this will displace any currently running rumble effect. +// * @param durationMs milliseconds to rumble for, or {@link #RUMBLE_DURATION_CONTINUOUS} +// */ +// public void rumble(int durationMs) { +// +// if (durationMs != RUMBLE_DURATION_CONTINUOUS) { +// durationMs = Math.max(0, durationMs); +// } +// +// RumbleEffect effect = new RumbleEffect.Builder().addStepInternal(1.0, 0, durationMs).build(); +// queueEffect(effect); +// } +// +// /** +// * Rumble the gamepad at a fixed rumble power for a certain duration +// * Calling this will displace any currently running rumble effect +// * @param rumble1 rumble power for rumble motor 1 (0.0 - 1.0) +// * @param rumble2 rumble power for rumble motor 2 (0.0 - 1.0) +// * @param durationMs milliseconds to rumble for, or {@link #RUMBLE_DURATION_CONTINUOUS} +// */ +// public void rumble(double rumble1, double rumble2, int durationMs) { +// +// if (durationMs != RUMBLE_DURATION_CONTINUOUS) { +// durationMs = Math.max(0, durationMs); +// } +// +// RumbleEffect effect = new RumbleEffect.Builder().addStepInternal(rumble1, rumble2, durationMs).build(); +// queueEffect(effect); +// } +// +// /** +// * Cancel the currently running rumble effect, if any +// */ +// public void stopRumble() { +// rumble(0,0,RUMBLE_DURATION_CONTINUOUS); +// } +// +// /** +// * Rumble the gamepad for a certain number of "blips" using predetermined blip timing +// * This will displace any currently running rumble effect. +// * @param count the number of rumble blips to perform +// */ +// public void rumbleBlips(int count) { +// RumbleEffect.Builder builder = new RumbleEffect.Builder(); +// +// for(int i = 0; i < count; i++) { +// builder.addStep(1.0,0,50).addStep(0,0,50); +// } +// +// queueEffect(builder.build()); +// } +// +// private void queueEffect(RumbleEffect effect) { +// // We need to make a new object here, because since the effect is queued and +// // not set immediately, if you called this function for two different gamepads +// // but passed in the same instance of a rumble effect object, then it could happen +// // that both rumble commands would be directed to the *same* gamepad. (Because of +// // the "user" field being modified before the queued command was serialized) +// RumbleEffect copy = new RumbleEffect(effect.steps); +// copy.user = userForEffects; +// rumbleQueue.offer(copy); +// nextRumbleApproxFinishTime = calcApproxRumbleFinishTime(copy); +// } +// +// private static final long RUMBLE_FINISH_TIME_FLAG_NOT_RUMBLING = -1; +// private static final long RUMBLE_FINISH_TIME_FLAG_INFINITE = Long.MAX_VALUE; +// +// /** +// * Returns an educated guess about whether there is a rumble action ongoing on this gamepad +// * @return an educated guess about whether there is a rumble action ongoing on this gamepad +// */ +// public boolean isRumbling() { +// if(nextRumbleApproxFinishTime == RUMBLE_FINISH_TIME_FLAG_NOT_RUMBLING) { +// return false; +// } else if(nextRumbleApproxFinishTime == RUMBLE_FINISH_TIME_FLAG_INFINITE) { +// return true; +// } else { +// return System.currentTimeMillis() < nextRumbleApproxFinishTime; +// } +// } +// +// private long calcApproxRumbleFinishTime(RumbleEffect effect) { +// // If the effect is only 1 step long and has an infinite duration... +// if(effect.steps.size() == 1 && effect.steps.get(0).duration == RUMBLE_DURATION_CONTINUOUS) { +// // If the power is zero, then that means the gamepad is being commanded to cease rumble +// if (effect.steps.get(0).large == 0 && effect.steps.get(0).small == 0) { +// return RUMBLE_FINISH_TIME_FLAG_NOT_RUMBLING; +// } else { // But if not, that means it's an infinite (continuous) rumble command +// return RUMBLE_FINISH_TIME_FLAG_INFINITE; +// } +// } else { // If the effect has more than one step (or one step with non-infinite duration) we need to sum the step times +// long time = System.currentTimeMillis(); +// long overhead = 50 /* rumbleGamepadsInterval in FtcEventLoopHandler */ + +// SendOnceRunnable.MS_BATCH_TRANSMISSION_INTERVAL + +// 5 /* Slop */; +// for(RumbleEffect.Step step : effect.steps) { +// time += step.duration; +// } +// time += overhead; +// return time; +// } +// } +// +// /** +// * Alias buttons so that XBOX & PS4 native button labels can be used in use code. +// * Should allow a team to program with whatever controllers they prefer, but +// * be able to swap controllers easily without changing code. +// */ +// protected void updateButtonAliases(){ +// // There is no assignment for touchpad because there is no equivalent on XBOX controllers. +// circle = b; +// cross = a; +// triangle = y; +// square = x; +// share = back; +// options = start; +// ps = guide; +// } +//} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Joystick.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Joystick.java new file mode 100644 index 0000000..770ee11 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Joystick.java @@ -0,0 +1,32 @@ +package org.firstinspires.ftc.teamcode.controller; + +public class Joystick { + public static double DEADZONE = 0.05; //small 5% deadzone for the new controlers + double x = 0; + double y = 0; + + public Joystick() {} + + public void update(double x, double y) { + this.x = x; + this.y = y; + } + + public double getX() { + if(x-DEADZONE) { + return 0; + } + else{ + return x; + } + } + + public double getY() { + if(y-DEADZONE) { + return 0; + } + else{ + return y; + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Trigger.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Trigger.java new file mode 100644 index 0000000..4f43bc4 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/controller/Trigger.java @@ -0,0 +1,33 @@ +package org.firstinspires.ftc.teamcode.controller; + +public class Trigger { + double value = 0; + double currentState; + double lastState; + boolean justPressed = false; + boolean justReleased = false; + + public Trigger() {} + + public void update(double value) { + lastState = currentState; + currentState = value; + + justPressed = currentState != 0 && lastState == 0; + justReleased = currentState == 0 && lastState != 0; + + this.value = value; + } + + public double getValue() { + return value; + } + + public boolean isJustPressed() { + return justPressed; + } + + public boolean isJustReleased() { + return justReleased; + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Camera.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Camera.java index a297fbd..04f617d 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Camera.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Camera.java @@ -9,17 +9,37 @@ import static org.firstinspires.ftc.teamcode.util.Constants.WEBCAM_WIDTH; import com.qualcomm.robotcore.hardware.HardwareMap; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.teamcode.util.CameraPosition; +import org.firstinspires.ftc.teamcode.vision.AprilTagDetectionPipeline; import org.firstinspires.ftc.teamcode.vision.Detection; import org.firstinspires.ftc.teamcode.vision.TargetingPipeline; +import org.openftc.apriltag.AprilTagDetection; import org.openftc.easyopencv.OpenCvCamera; import org.openftc.easyopencv.OpenCvCameraFactory; +import java.util.ArrayList; + // Class for the camera public class Camera { + + private float decimation; + private boolean needToSetDecimation; + int numFramesWithoutDetection = 0; + private boolean signalWebcamInitialized; + private OpenCvCamera signalWebcam; private HardwareMap hardwareMap; private OpenCvCamera targetingCamera; private TargetingPipeline targetingPipeline; private boolean targetingCameraInitialized; + AprilTagDetectionPipeline aprilTagDetectionPipeline; + ArrayList detections; + static final double FEET_PER_METER = 3.28084; + public CameraPosition cameraPosition; + private final Object decimationSync = new Object(); + final float DECIMATION_HIGH = 3; + final float DECIMATION_LOW = 2; + final float THRESHOLD_HIGH_DECIMATION_RANGE_METERS = 1.0f; + final int THRESHOLD_NUM_FRAMES_NO_DETECTION_BEFORE_LOW_DECIMATION = 4; // Constructor public Camera(HardwareMap hardwareMap) { @@ -53,8 +73,57 @@ public class Camera { } } - // Get the Red Goal Detection + // detect colors public Detection getRed() { return (targetingCameraInitialized ? targetingPipeline.getRed() : INVALID_DETECTION); } + + public Detection getBlue() { + return (targetingCameraInitialized ? targetingPipeline.getBlue() : INVALID_DETECTION); + } + + public Detection getBlack() { + return (targetingCameraInitialized ? targetingPipeline.getBlack() : INVALID_DETECTION); + } + + //return frame rate + public int getFrameCount() { + if (signalWebcamInitialized) { + return signalWebcam.getFrameCount(); + } else { + return 0; + } + } + + public int getMarkerId() { + detections = aprilTagDetectionPipeline.getLatestDetections(); + + // If there's been a new frame... + if (detections != null) { + // If we don't see any tags + if (detections.size() == 0) { + numFramesWithoutDetection++; + + // If we haven't seen a tag for a few frames, lower the decimation + // so we can hopefully pick one up if we're e.g. far back + if (numFramesWithoutDetection >= THRESHOLD_NUM_FRAMES_NO_DETECTION_BEFORE_LOW_DECIMATION) { + aprilTagDetectionPipeline.setDecimation(DECIMATION_LOW); + } + } + // We do see tags! + else { + numFramesWithoutDetection = 0; + + // If the target is within 1 meter, turn on high decimation to + // increase the frame rate + if (detections.get(0).pose.z < THRESHOLD_HIGH_DECIMATION_RANGE_METERS) { + aprilTagDetectionPipeline.setDecimation(DECIMATION_HIGH); + } + + return detections.get(0).id; + + } + } + return -1; + } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Drive.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Drive.java index 50bd0ee..241b117 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Drive.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Drive.java @@ -1,28 +1,101 @@ -package org.firstinspires.ftc.teamcode.hardware; - -import com.qualcomm.robotcore.hardware.DcMotor; -import com.qualcomm.robotcore.hardware.Gamepad; -import com.qualcomm.robotcore.hardware.HardwareMap; - -public class Drive { - - private DcMotor frontLeft; - private DcMotor frontRight; - private DcMotor backLeft; - private DcMotor backRight; - - public Drive(HardwareMap hardwareMap) { - this.init(hardwareMap); - } - - private void init(HardwareMap hardwareMap) { - this.frontLeft = hardwareMap.get(DcMotor.class, "frontLeft"); - this.frontRight = hardwareMap.get(DcMotor.class, "frontRight"); - this.backLeft = hardwareMap.get(DcMotor.class, "backLeft"); - this.backRight = hardwareMap.get(DcMotor.class, "backRight"); - } - - public void setInput(Gamepad gamepad1,Gamepad gamepad2) { - - } -} +//package org.firstinspires.ftc.teamcode.hardware; +// +//import com.acmerobotics.roadrunner.control.PIDCoefficients; +//import com.acmerobotics.roadrunner.drive.DriveSignal; +//import com.acmerobotics.roadrunner.geometry.Pose2d; +//import com.acmerobotics.roadrunner.trajectory.BaseTrajectoryBuilder; +//import com.acmerobotics.roadrunner.trajectory.TrajectoryBuilder; +//import com.acmerobotics.roadrunner.trajectory.constraints.AngularVelocityConstraint; +//import com.acmerobotics.roadrunner.trajectory.constraints.MecanumVelocityConstraint; +//import com.acmerobotics.roadrunner.trajectory.constraints.MinVelocityConstraint; +//import com.acmerobotics.roadrunner.trajectory.constraints.ProfileAccelerationConstraint; +//import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryAccelerationConstraint; +//import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryVelocityConstraint; +//import com.qualcomm.robotcore.hardware.DcMotor; +//import com.qualcomm.robotcore.hardware.DcMotorSimple; +//import com.qualcomm.robotcore.hardware.Gamepad; +//import com.qualcomm.robotcore.hardware.HardwareMap; +// +//import org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants; +//import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequenceRunner; +// +//import java.util.Arrays; +// +//public class Drive { +// +// DcMotor frontLeft; +// DcMotor frontRight; +// DcMotor backLeft; +// DcMotor backRight; +// public static PIDCoefficients TRANSLATIONAL_PID = new PIDCoefficients(10, 0, 0); +// public static PIDCoefficients HEADING_PID = new PIDCoefficients(8, 0, 0); +// +// private static final TrajectoryVelocityConstraint VEL_CONSTRAINT = getVelocityConstraint(DriveConstants.MAX_VEL, DriveConstants.MAX_ANG_VEL, DriveConstants.TRACK_WIDTH); +// private static final TrajectoryAccelerationConstraint ACCEL_CONSTRAINT = getAccelerationConstraint(DriveConstants.MAX_ACCEL); +// private TrajectorySequenceRunner trajectorySequenceRunner; +// +// public Drive(HardwareMap hardwareMap) { +// this.init(hardwareMap); +// } +// +// private void init(HardwareMap hardwareMap) { +// this.frontLeft = hardwareMap.get(DcMotor.class, "frontLeft"); +// frontLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); +// frontLeft.setMode(DcMotor.RunMode.RUN_USING_ENCODER); +// this.frontRight = hardwareMap.get(DcMotor.class, "frontRight"); +// frontRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); +// frontRight.setMode(DcMotor.RunMode.RUN_USING_ENCODER); +// this.backLeft = hardwareMap.get(DcMotor.class, "backLeft"); +// backLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); +// backLeft.setMode(DcMotor.RunMode.RUN_USING_ENCODER); +// this.backRight = hardwareMap.get(DcMotor.class, "backRight"); +// backRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); +// backRight.setMode(DcMotor.RunMode.RUN_USING_ENCODER); +// this.frontLeft.setDirection(DcMotorSimple.Direction.FORWARD); +// this.backRight.setDirection(DcMotorSimple.Direction.REVERSE); +// this.frontRight.setDirection(DcMotorSimple.Direction.REVERSE); +// this.backLeft.setDirection(DcMotorSimple.Direction.FORWARD); +// } +// +// public void setInput(Gamepad gamepad1,Gamepad gamepad2) { +// float x = -gamepad1.left_stick_x; +// float y = gamepad1.left_stick_y; +// float z = -gamepad1.right_stick_x; +// float flPower = x + y + z; +// float frPower = -x + y - z; +// float blPower = -x + y + z; +// float brPower = x + y - z; +// +// float max = Math.max(1, Math.max(Math.max(Math.abs(blPower), Math.abs(brPower)), Math.max(Math.abs(flPower),Math.abs(frPower)))); +// float scale = 1-((max-1) / max); +// flPower *= scale; +// frPower *= scale; +// blPower *= scale; +// brPower *= scale; +// +// frontLeft.setPower(flPower); +// backLeft.setPower(blPower); +// frontRight.setPower(frPower); +// backRight.setPower(brPower); +// } +// +// public void update() { +// DriveSignal signal = trajectorySequenceRunner.update(getPoseEstimate(), getPoseVelocity()); +// if (signal != null) setDriveSignal(signal); +// } +// +// public TrajectoryBuilder trajectoryBuilder(Pose2d startPose) { +// return new TrajectoryBuilder(startPose, VEL_CONSTRAINT, ACCEL_CONSTRAINT); +// } +// +// public static TrajectoryVelocityConstraint getVelocityConstraint(double maxVel, double maxAngularVel, double trackWidth) { +// return new MinVelocityConstraint(Arrays.asList( +// new AngularVelocityConstraint(maxAngularVel), +// new MecanumVelocityConstraint(maxVel, trackWidth) +// )); +// } +// +// public static TrajectoryAccelerationConstraint getAccelerationConstraint(double maxAccel) { +// return new ProfileAccelerationConstraint(maxAccel); +// } +//} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Intake.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Intake.java new file mode 100644 index 0000000..aaa90d7 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Intake.java @@ -0,0 +1,74 @@ +package org.firstinspires.ftc.teamcode.hardware; + +import com.acmerobotics.dashboard.config.Config; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.HardwareMap; +import com.qualcomm.robotcore.hardware.Servo; + +@Config +public class Intake { // TODO done in theory, but need to get the actual servo positions &&&& do the Sensor stuff + private Servo rServo; + private Servo lServo; + private DcMotor dcMotor; + private Position pos = Position.UP; + + public enum Position { + STACK1, STACK2, STACK3, STACK4, STACK5, UP; + + public Position nextPosition() { + int nextOne = (this.ordinal() + 1) % Position.values().length; + return Position.values()[nextOne]; + } + + public Position previousPosition() { + int backOne = (this.ordinal() - 1) % Position.values().length; + return Position.values()[backOne]; + } + } + + //Position + public static double stack1 = 0.49; + public static double stack2 = 0.50; + public static double stack3 = 0.51; + public static double stack4 = 0.52; + public static double stack5 = 0.53; + public static double up = 0.551; + public static double motorPower = 0; + + public Intake(HardwareMap hardwareMap) { + lServo = hardwareMap.get(Servo.class, "Left Servo"); + lServo.setDirection(Servo.Direction.REVERSE); + rServo = hardwareMap.get(Servo.class, "Right Servo"); + dcMotor = hardwareMap.get(DcMotor.class, "Intake Motor"); + } + + public void setpos(Position stack) { + if (stack == Position.STACK1) { + lServo.setPosition(stack1); + rServo.setPosition(stack1); + } else if (stack == Position.STACK2) { + lServo.setPosition(stack2); + rServo.setPosition(stack2); + } else if (stack == Position.STACK3) { + lServo.setPosition(stack3); + rServo.setPosition(stack3); + } else if (stack == Position.STACK4) { + lServo.setPosition(stack4); + rServo.setPosition(stack4); + } else if (stack == Position.STACK5) { + lServo.setPosition(stack5); + rServo.setPosition(stack5); + } else if (stack == Position.UP) { + lServo.setPosition(up); + rServo.setPosition(up); + } + } + + public void setDcMotor(double pwr) { + dcMotor.setPower(pwr); + } + + public String getTelemetry() { + return "lServo: "+lServo.getPosition()+"rServo: "+rServo.getPosition()+"dcMotor: "+dcMotor.getPower(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Robot.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Robot.java index 0291831..02dd6c2 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Robot.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/hardware/Robot.java @@ -1,20 +1,39 @@ package org.firstinspires.ftc.teamcode.hardware; +import com.acmerobotics.dashboard.config.Config; import com.qualcomm.robotcore.hardware.HardwareMap; -public class Robot { +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; +import org.firstinspires.ftc.teamcode.roadrunner.util.Encoder; - private Drive drive; +@Config +public class Robot { + //set to public Drive to revert + public SampleMecanumDrive drive; + public Camera camera; + public Intake intake; + private boolean camEnabled = true; public Robot(HardwareMap hardwareMap) { - this.init(hardwareMap); + //change this to new Drive to revert + drive = new SampleMecanumDrive(hardwareMap); + camera = new Camera(hardwareMap); + camera.initTargetingCamera(); + intake = new Intake(hardwareMap); + camEnabled = true; + } + + public void update(double runTime) { + drive.update(); } - private void init(HardwareMap hardwareMap) { - this.drive = new Drive(hardwareMap); + public String getTelemetry() { + Encoder slide = null; + return String.format("position: %s", slide.getCurrentPosition()); } - public Drive getDrive() { + //set to public Drive to revert + public SampleMecanumDrive getDrive() { return this.drive; } -} +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/AbstractAuto.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/AbstractAuto.java new file mode 100644 index 0000000..6d78026 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/AbstractAuto.java @@ -0,0 +1,50 @@ +package org.firstinspires.ftc.teamcode.opmode.autonomous; + +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.hardware.Robot; + +import java.util.ArrayList; + +public abstract class AbstractAuto extends LinearOpMode { + + public Robot robot; + private int teamElementLocation = 2; + private double currentRuntime; + + @Override + public void runOpMode() { + + telemetry.addLine("Initializing Robot..."); + telemetry.update(); + robot = new Robot(hardwareMap); + makeTrajectories(); + + while (robot.camera.getFrameCount() < 1) { + idle(); + } + + while (!(isStarted() || isStopRequested())) { + currentRuntime = getRuntime(); + robot.update(currentRuntime); + + int newLocation = robot.camera.getMarkerId(); + if (newLocation != -1) { + teamElementLocation = newLocation; + } + + telemetry.addLine("Initialized"); + telemetry.addLine("Randomization: " + teamElementLocation); + telemetry.addLine(robot.getTelemetry()); + telemetry.update(); + } + } + + public abstract void setAlliance(); + + public abstract void makeTrajectories(); + + public abstract void setCameraPosition(); + + public abstract boolean useCamera(); +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/StartFromLeftCenterSpike.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/StartFromLeftCenterSpike.java new file mode 100644 index 0000000..c70d97d --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/autonomous/StartFromLeftCenterSpike.java @@ -0,0 +1,65 @@ +package org.firstinspires.ftc.teamcode.opmode.autonomous; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.hardware.HardwareMap; + +import org.firstinspires.ftc.teamcode.hardware.Camera; +import org.firstinspires.ftc.teamcode.hardware.Drive; +import org.firstinspires.ftc.teamcode.hardware.Robot; +import org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; +import org.firstinspires.ftc.teamcode.util.CameraPosition; + +@Autonomous(name = "Start From Left Wall", group = "Left Start", preselectTeleOp = "Khang Main") +public class StartFromLeftCenterSpike extends AbstractAuto { + + //set to public Drive to revert + public SampleMecanumDrive drive; + public Robot robot; + public Camera camera; + private boolean camEnabled = true; + public CameraPosition cameraPosition; + private int teamElementLocation = 2; + //Steps + public Trajectory start; + public Trajectory step1; + public Trajectory step2; + + @Override + public void setAlliance() {} + + @Override + public void setCameraPosition() { + cameraPosition = CameraPosition.CENTER; + } + + @Override + public boolean useCamera() { + return true; + } + + public void Robot(HardwareMap hardwareMap) { + //set to new Drive to revert + drive = new SampleMecanumDrive(hardwareMap); + camera = new Camera(hardwareMap); + camera.initTargetingCamera(); + camEnabled = true; + } + + @Override + public void makeTrajectories() { + + // positions + Pose2d start = new Pose2d(-65.125,-48,Math.toRadians(-90)); + Pose2d step1 = new Pose2d(-24,-48,Math.toRadians(0)); + Pose2d step2 = new Pose2d(-24,-48,Math.toRadians(0)); + + this.start = robot.drive.trajectoryBuilder(start) + .lineToSplineHeading(step1, + SampleMecanumDrive.getVelocityConstraint(60, DriveConstants.MAX_ANG_VEL, DriveConstants.TRACK_WIDTH), + SampleMecanumDrive.getAccelerationConstraint(DriveConstants.MAX_ACCEL) + ) + .build(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/AbstractTeleOp.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/AbstractTeleOp.java new file mode 100644 index 0000000..d0dda3d --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/AbstractTeleOp.java @@ -0,0 +1,150 @@ +package org.firstinspires.ftc.teamcode.opmode.teleop; + +import static java.lang.Math.PI; + +import com.acmerobotics.dashboard.config.Config; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; + +import org.firstinspires.ftc.teamcode.controller.Controller; +import org.firstinspires.ftc.teamcode.hardware.Robot; + +@Config +public abstract class AbstractTeleOp extends OpMode { + private Robot robot; + Controller driver1; + Controller driver2; + + public static double drivebaseThrottle = 0.6; + public static double drivebaseTurbo = 1.0; + public static int heightIncrement = 20; + + Pose2d robot_pos; + double robot_x, robot_y, robot_heading; + + // auto align variables + double headingPID; + public static double headingP = 0.01; + public static double headingI = 0.03; + public static double headingD = 0.0005; + + double strafePID; + public static double strafeP = 0.05; + public static double strafeI = 0; + public static double strafeD = 0.01; + + double robot_y_pos; + boolean fixed90Toggle = false; + boolean fixed0Toggle = false; + +// public static double robot_width = 12; +// public static double robot_length = 12; +// public static double robot_radius = 6; +// +// public static double groundJuncRadius = 6; +// public static double coneRadius = 4; +// +// public static double armWait = 0.2; + +// private double timeSinceOpened = 0; // for claw +// private double timeSinceClosed = 0; + +// private int delayState = 0; // for arm +// private double delayStart = 0; +// private boolean doArmDelay = false; // for arm + private boolean isAutoClose = true; + + @Override + public void init() { + robot = new Robot(hardwareMap); + driver1 = new Controller(gamepad1); + driver2 = new Controller(gamepad2); + } + + @Override + public void init_loop() { +// if (robot.camera.getFrameCount() < 1) { +// telemetry.addLine("Initializing Robot..."); +// } else { +// telemetry.addLine("Initialized"); +// } + telemetry.addLine("Initialized"); + telemetry.addLine(robot.getTelemetry()); + telemetry.update(); + } + + private int getQuadrant(double angle) { + if (0 < angle && angle < PI/2.0) { + return 1; + } else if (PI/2.0 < angle && angle < PI) { + return 2; + } else if (PI < angle && angle < 3*PI/2.0) { + return 3; + } else { + return 4; + } + } + + @Override + public void loop() { + // robot position update + //robot_pos = robot.drive.getPoseEstimate(); + //robot_x = robot_pos.getX(); + //robot_y = robot_pos.getY(); + //robot_heading = robot_pos.getHeading(); // in radians + + // driver 1 controls + //driver1.update(); + //driver2.update(); + double x = -driver1.getLeftStick().getY(); + double y = driver1.getLeftStick().getX(); + double z = -driver1.getRightStick().getX(); + +// figure out if a toggle is present + if (driver1.getRightBumper().isPressed()) { + if (!fixed90Toggle) { + //robot_y_pos = robot.drive.getPoseEstimate().getX(); + } + fixed90Toggle = true; + } else { + fixed90Toggle = false; + } + + if (!fixed90Toggle && driver1.getLeftBumper().isPressed()) { + if (!fixed0Toggle) { + //robot_y_pos = robot.drive.getPoseEstimate().getY(); + } + fixed0Toggle = true; + } else { + fixed0Toggle = false; + } + + telemetry.addLine("Wanted Position: "+robot_y_pos); + telemetry.addLine("Actual Position: "+robot_y); + telemetry.addLine("PID: "+strafePID); + + // turbo + if (driver1.getLeftBumper().isPressed() || driver1.getRightBumper().isPressed()) { + x *= drivebaseTurbo; + y *= drivebaseTurbo; + z *= drivebaseTurbo; + } else { + x *= drivebaseThrottle; + y *= drivebaseThrottle; + z *= drivebaseThrottle; + } + + // actually set power + if (fixed0Toggle || fixed90Toggle) { + //robot.drive.setWeightedDrivePower(new Pose2d(x, 0, headingPID)); + } else { + //robot.drive.setWeightedDrivePower(new Pose2d(x, y, z)); + } + + // update and telemetry + //robot.update(getRuntime()); + + telemetry.addLine(robot.getTelemetry()); + telemetry.update(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/KhangMain.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/KhangMain.java new file mode 100644 index 0000000..1e52d99 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/teleop/KhangMain.java @@ -0,0 +1,71 @@ +package org.firstinspires.ftc.teamcode.opmode.teleop; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.teamcode.controller.Controller; +import org.firstinspires.ftc.teamcode.hardware.Camera; +import org.firstinspires.ftc.teamcode.hardware.Intake; +import org.firstinspires.ftc.teamcode.hardware.Robot; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.DcMotorSimple; +import com.qualcomm.robotcore.hardware.Gamepad; +import com.qualcomm.robotcore.hardware.HardwareMap; + +@TeleOp(name = "Khang Main", group = "OpModes") +public class KhangMain extends OpMode { + + private Intake.Position Currentpos = Intake.Position.UP; + private Robot robot; + private Controller controller2; + + @Override + public void init() { + this.robot = new Robot(hardwareMap); + controller2 = new Controller(gamepad2); + } + + @Override + public void loop() { +// this.robot.getDrive().setInput(gamepad1, gamepad2); + double x = gamepad1.left_stick_x; + double y = -gamepad1.left_stick_y; + double z = gamepad1.right_stick_x; + robot.drive.setWeightedDrivePower(new Pose2d(x, y, z)); + robot.intake.setDcMotor(gamepad2.right_trigger); + robot.intake.setpos(Currentpos); + controller2.update(); + + if (controller2.getLeftBumper().isJustPressed()) { + Currentpos = Currentpos.nextPosition(); + } + + if (controller2.getRightBumper().isJustPressed()) { + Currentpos = Currentpos.previousPosition(); + } + + + + // Read pose +// Pose2d poseEstimate = drive.getPoseEstimate(); +// +//// Create a vector from the gamepad x/y inputs +//// Then, rotate that vector by the inverse of that heading +// Vector2d input = new Vector2d( +// -gamepad1.left_stick_y, +// -gamepad1.left_stick_x +// ).rotated(-poseEstimate.getHeading()); +// +//// Pass in the rotated input + right stick value for rotation +//// Rotation is not part of the rotated input thus must be passed in separately +// drive.setWeightedDrivePower( +// new Pose2d( +// input.getX(), +// input.getY(), +// -gamepad1.right_stick_x +// ) +// ); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/DriveConstants.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/DriveConstants.java new file mode 100644 index 0000000..d2f6fb0 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/DriveConstants.java @@ -0,0 +1,89 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive; + +import com.acmerobotics.dashboard.config.Config; +import com.qualcomm.robotcore.hardware.PIDFCoefficients; + +/* + * Constants shared between multiple drive types. + * + * TODO: Tune or adjust the following constants to fit your robot. Note that the non-final + * fields may also be edited through the dashboard (connect to the robot's WiFi network and + * navigate to https://192.168.49.1:8080/dash). Make sure to save the values here after you + * adjust them in the dashboard; **config variable changes don't persist between app restarts**. + * + * These are not the only parameters; some are located in the localizer classes, drive base classes, + * and op modes themselves. + */ +@Config +public class DriveConstants { + + /* + * These are motor constants that should be listed online for your motors. + */ + public static final double TICKS_PER_REV = 384.5; + public static final double MAX_RPM = 435; + + /* + * Set RUN_USING_ENCODER to true to enable built-in hub velocity control using drive encoders. + * Set this flag to false if drive encoders are not present and an alternative localization + * method is in use (e.g., tracking wheels). + * + * If using the built-in motor velocity PID, update MOTOR_VELO_PID with the tuned coefficients + * from DriveVelocityPIDTuner. + */ + public static final boolean RUN_USING_ENCODER = true; + public static PIDFCoefficients MOTOR_VELO_PID = new PIDFCoefficients(0, 0, 0, + getMotorVelocityF(MAX_RPM / 60 * TICKS_PER_REV)); + + /* + * These are physical constants that can be determined from your robot (including the track + * width; it will be tune empirically later although a rough estimate is important). Users are + * free to chose whichever linear distance unit they would like so long as it is consistently + * used. The default values were selected with inches in mind. Road runner uses radians for + * angular distances although most angular parameters are wrapped in Math.toRadians() for + * convenience. Make sure to exclude any gear ratio included in MOTOR_CONFIG from GEAR_RATIO. + */ + public static double WHEEL_RADIUS = 1.8045; // in + public static double GEAR_RATIO = 1; // output (wheel) speed / input (motor) speed + public static double TRACK_WIDTH = 15.75; // in + + /* + * These are the feedforward parameters used to model the drive motor behavior. If you are using + * the built-in velocity PID, *these values are fine as is*. However, if you do not have drive + * motor encoders or have elected not to use them for velocity control, these values should be + * empirically tuned. + */ +// public static double kV = 1.0 / rpmToVelocity(MAX_RPM); + + public static double kV = 0.013; + public static double kA = 0.003; + public static double kStatic = 0; + + /* + * These values are used to generate the trajectories for you robot. To ensure proper operation, + * the constraints should never exceed ~80% of the robot's actual capabilities. While Road + * Runner is designed to enable faster autonomous motion, it is a good idea for testing to start + * small and gradually increase them later after everything is working. All distance units are + * inches. + */ + // old is 35 35 120 120 + // new is 70 45 90 90 + public static double MAX_VEL = 35; + public static double MAX_ACCEL = 35; + public static double MAX_ANG_VEL = Math.toRadians(90); + public static double MAX_ANG_ACCEL = Math.toRadians(90); + + + public static double encoderTicksToInches(double ticks) { + return WHEEL_RADIUS * 2 * Math.PI * GEAR_RATIO * ticks / TICKS_PER_REV; + } + + public static double rpmToVelocity(double rpm) { + return rpm * GEAR_RATIO * 2 * Math.PI * WHEEL_RADIUS / 60.0; + } + + public static double getMotorVelocityF(double ticksPerSecond) { + // see https://docs.google.com/document/d/1tyWrXDfMidwYyP_5H4mZyVgaEswhOC35gvdmP-V-5hA/edit#heading=h.61g9ixenznbx + return 32767 / ticksPerSecond; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/MouseOdometry.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/MouseOdometry.java new file mode 100644 index 0000000..e93cb3f --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/MouseOdometry.java @@ -0,0 +1,5 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive;//package org.firstinspires.ftc.teamcode.roadrunner.drive; +//import java.awt. +// +//public class MouseOdometry { +//} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleMecanumDrive.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleMecanumDrive.java new file mode 100644 index 0000000..dee69ec --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleMecanumDrive.java @@ -0,0 +1,326 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive; + +import androidx.annotation.NonNull; + +import com.acmerobotics.dashboard.config.Config; +import com.acmerobotics.roadrunner.control.PIDCoefficients; +import com.acmerobotics.roadrunner.drive.DriveSignal; +import com.acmerobotics.roadrunner.drive.MecanumDrive; +import com.acmerobotics.roadrunner.followers.HolonomicPIDVAFollower; +import com.acmerobotics.roadrunner.followers.TrajectoryFollower; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.acmerobotics.roadrunner.trajectory.TrajectoryBuilder; +import com.acmerobotics.roadrunner.trajectory.constraints.AngularVelocityConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.MecanumVelocityConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.MinVelocityConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.ProfileAccelerationConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryAccelerationConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryVelocityConstraint; +import com.qualcomm.hardware.bosch.BNO055IMU; +import com.qualcomm.hardware.lynx.LynxModule; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.DcMotorEx; +import com.qualcomm.robotcore.hardware.DcMotorSimple; +import com.qualcomm.robotcore.hardware.Gamepad; +import com.qualcomm.robotcore.hardware.HardwareMap; +import com.qualcomm.robotcore.hardware.PIDFCoefficients; +import com.qualcomm.robotcore.hardware.VoltageSensor; +import com.qualcomm.robotcore.hardware.configuration.typecontainers.MotorConfigurationType; + +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequence; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequenceBuilder; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequenceRunner; +import org.firstinspires.ftc.teamcode.roadrunner.util.LynxModuleUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Config +public class SampleMecanumDrive extends MecanumDrive { + public static PIDCoefficients TRANSLATIONAL_PID = new PIDCoefficients(10, 0, 0); + public static PIDCoefficients HEADING_PID = new PIDCoefficients(8, 0, 0); + + public static double LATERAL_MULTIPLIER = 1; + + public static double VX_WEIGHT = 1; + public static double VY_WEIGHT = 1; + public static double OMEGA_WEIGHT = 1; + + private TrajectorySequenceRunner trajectorySequenceRunner; + + private static final TrajectoryVelocityConstraint VEL_CONSTRAINT = getVelocityConstraint(DriveConstants.MAX_VEL, DriveConstants.MAX_ANG_VEL, DriveConstants.TRACK_WIDTH); + private static final TrajectoryAccelerationConstraint ACCEL_CONSTRAINT = getAccelerationConstraint(DriveConstants.MAX_ACCEL); + + private TrajectoryFollower follower; + + private DcMotorEx leftFront, leftRear, rightRear, rightFront; + private List motors; + + private BNO055IMU imu; + private VoltageSensor batteryVoltageSensor; + + public SampleMecanumDrive(HardwareMap hardwareMap) { + super(DriveConstants.kV, DriveConstants.kA, DriveConstants.kStatic, DriveConstants.TRACK_WIDTH, DriveConstants.TRACK_WIDTH, LATERAL_MULTIPLIER); + + follower = new HolonomicPIDVAFollower(TRANSLATIONAL_PID, TRANSLATIONAL_PID, HEADING_PID, + new Pose2d(0.5, 0.5, Math.toRadians(5)), 0.1); + + LynxModuleUtil.ensureMinimumFirmwareVersion(hardwareMap); + + batteryVoltageSensor = hardwareMap.voltageSensor.iterator().next(); + + for (LynxModule module : hardwareMap.getAll(LynxModule.class)) { + module.setBulkCachingMode(LynxModule.BulkCachingMode.AUTO); + } + + // TODO: adjust the names of the following hardware devices to match your configuration + imu = hardwareMap.get(BNO055IMU.class, "imu"); + BNO055IMU.Parameters parameters = new BNO055IMU.Parameters(); + parameters.angleUnit = BNO055IMU.AngleUnit.RADIANS; + imu.initialize(parameters); + + // TODO: If the hub containing the IMU you are using is mounted so that the "REV" logo does + // not face up, remap the IMU axes so that the z-axis points upward (normal to the floor.) + // + // | +Z axis + // | + // | + // | + // _______|_____________ +Y axis + // / |_____________/|__________ + // / REV / EXPANSION // + // / / HUB // + // /_______/_____________// + // |_______/_____________|/ + // / + // / +X axis + // + // This diagram is derived from the axes in section 3.4 https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bno055-ds000.pdf + // and the placement of the dot/orientation from https://docs.revrobotics.com/rev-control-system/control-system-overview/dimensions#imu-location + // + // For example, if +Y in this diagram faces downwards, you would use AxisDirection.NEG_Y. + // BNO055IMUUtil.remapZAxis(imu, AxisDirection.NEG_Y); + + leftFront = hardwareMap.get(DcMotorEx.class, "frontLeft"); + leftRear = hardwareMap.get(DcMotorEx.class, "backLeft"); + rightRear = hardwareMap.get(DcMotorEx.class, "backRight"); + rightFront = hardwareMap.get(DcMotorEx.class, "frontRight"); + + motors = Arrays.asList(leftFront, leftRear, rightRear, rightFront); + + for (DcMotorEx motor : motors) { + MotorConfigurationType motorConfigurationType = motor.getMotorType().clone(); + motorConfigurationType.setAchieveableMaxRPMFraction(1.0); + motor.setMotorType(motorConfigurationType); + } + + + if (DriveConstants.RUN_USING_ENCODER) { + setMode(DcMotor.RunMode.RUN_USING_ENCODER); + } + + setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + + if (DriveConstants.RUN_USING_ENCODER && DriveConstants.MOTOR_VELO_PID != null) { + setPIDFCoefficients(DcMotor.RunMode.RUN_USING_ENCODER, DriveConstants.MOTOR_VELO_PID); + } + + // TODO: reverse any motors using DcMotor.setDirection() + rightFront.setDirection(DcMotorSimple.Direction.REVERSE); + rightRear.setDirection(DcMotorSimple.Direction.REVERSE); + leftRear.setDirection(DcMotorSimple.Direction.REVERSE); + leftFront.setDirection(DcMotorSimple.Direction.REVERSE); + + // TODO: if desired, use setLocalizer() to change the localization method + // for instance, setLocalizer(new ThreeTrackingWheelLocalizer(...)); + + setLocalizer(new TwoWheelTrackingLocalizer(hardwareMap, this)); +// setLocalizer(new StandardTrackingWheelLocalizer(hardwareMap)); + + trajectorySequenceRunner = new TrajectorySequenceRunner(follower, HEADING_PID); + } + + public TrajectoryBuilder trajectoryBuilder(Pose2d startPose) { + return new TrajectoryBuilder(startPose, VEL_CONSTRAINT, ACCEL_CONSTRAINT); + } + + public TrajectoryBuilder trajectoryBuilder(Pose2d startPose, boolean reversed) { + return new TrajectoryBuilder(startPose, reversed, VEL_CONSTRAINT, ACCEL_CONSTRAINT); + } + + public TrajectoryBuilder trajectoryBuilder(Pose2d startPose, double startHeading) { + return new TrajectoryBuilder(startPose, startHeading, VEL_CONSTRAINT, ACCEL_CONSTRAINT); + } + + public TrajectorySequenceBuilder trajectorySequenceBuilder(Pose2d startPose) { + return new TrajectorySequenceBuilder( + startPose, + VEL_CONSTRAINT, ACCEL_CONSTRAINT, + DriveConstants.MAX_ANG_VEL, DriveConstants.MAX_ANG_ACCEL + ); + } + + public void turnAsync(double angle) { + trajectorySequenceRunner.followTrajectorySequenceAsync( + trajectorySequenceBuilder(getPoseEstimate()) + .turn(angle) + .build() + ); + } + + public void turn(double angle) { + turnAsync(angle); + waitForIdle(); + } + + public void followTrajectoryAsync(Trajectory trajectory) { + trajectorySequenceRunner.followTrajectorySequenceAsync( + trajectorySequenceBuilder(trajectory.start()) + .addTrajectory(trajectory) + .build() + ); + } + + public void followTrajectory(Trajectory trajectory) { + followTrajectoryAsync(trajectory); + waitForIdle(); + } + + public void followTrajectorySequenceAsync(TrajectorySequence trajectorySequence) { + trajectorySequenceRunner.followTrajectorySequenceAsync(trajectorySequence); + } + + public void followTrajectorySequence(TrajectorySequence trajectorySequence) { + followTrajectorySequenceAsync(trajectorySequence); + waitForIdle(); + } + + public Pose2d getLastError() { + return trajectorySequenceRunner.getLastPoseError(); + } + + public void update() { + updatePoseEstimate(); + DriveSignal signal = trajectorySequenceRunner.update(getPoseEstimate(), getPoseVelocity()); + if (signal != null) setDriveSignal(signal); + } + + public void waitForIdle() { + while (!Thread.currentThread().isInterrupted() && isBusy()) + update(); + } + + public boolean isBusy() { + return trajectorySequenceRunner.isBusy(); + } + + public void setMode(DcMotor.RunMode runMode) { + for (DcMotorEx motor : motors) { + motor.setMode(runMode); + } + } + + public void setZeroPowerBehavior(DcMotor.ZeroPowerBehavior zeroPowerBehavior) { + for (DcMotorEx motor : motors) { + motor.setZeroPowerBehavior(zeroPowerBehavior); + } + } + + public void setPIDFCoefficients(DcMotor.RunMode runMode, PIDFCoefficients coefficients) { + PIDFCoefficients compensatedCoefficients = new PIDFCoefficients( + coefficients.p, coefficients.i, coefficients.d, + coefficients.f * 12 / batteryVoltageSensor.getVoltage() + ); + + for (DcMotorEx motor : motors) { + motor.setPIDFCoefficients(runMode, compensatedCoefficients); + } + } + + public void setWeightedDrivePower(Pose2d drivePower) { + Pose2d vel = drivePower; + + if (Math.abs(drivePower.getX()) + Math.abs(drivePower.getY()) + + Math.abs(drivePower.getHeading()) > 1) { + // re-normalize the powers according to the weights + double denom = VX_WEIGHT * Math.abs(drivePower.getX()) + + VY_WEIGHT * Math.abs(drivePower.getY()) + + OMEGA_WEIGHT * Math.abs(drivePower.getHeading()); + + vel = new Pose2d( + VX_WEIGHT * drivePower.getX(), + VY_WEIGHT * drivePower.getY(), + OMEGA_WEIGHT * drivePower.getHeading() + ).div(denom); + } + + setDrivePower(vel); + } + + @NonNull + @Override + public List getWheelPositions() { + List wheelPositions = new ArrayList<>(); + for (DcMotorEx motor : motors) { + wheelPositions.add(DriveConstants.encoderTicksToInches(motor.getCurrentPosition())); + } + return wheelPositions; + } + + @Override + public List getWheelVelocities() { + List wheelVelocities = new ArrayList<>(); + for (DcMotorEx motor : motors) { + wheelVelocities.add(DriveConstants.encoderTicksToInches(motor.getVelocity())); + } + return wheelVelocities; + } + + @Override + public void setMotorPowers(double v, double v1, double v2, double v3) { + leftFront.setPower(v); + leftRear.setPower(v1); + rightRear.setPower(v2); + rightFront.setPower(v3); + } + + @Override + public double getRawExternalHeading() { + return imu.getAngularOrientation().firstAngle; + } + + @Override + public Double getExternalHeadingVelocity() { + // To work around an SDK bug, use -zRotationRate in place of xRotationRate + // and -xRotationRate in place of zRotationRate (yRotationRate behaves as + // expected). This bug does NOT affect orientation. + // + // See https://github.com/FIRST-Tech-Challenge/FtcRobotController/issues/251 for details. + return (double) -imu.getAngularVelocity().zRotationRate; + } + + public static TrajectoryVelocityConstraint getVelocityConstraint(double maxVel, double maxAngularVel, double trackWidth) { + return new MinVelocityConstraint(Arrays.asList( + new AngularVelocityConstraint(maxAngularVel), + new MecanumVelocityConstraint(maxVel, trackWidth) + )); + } + + public static TrajectoryAccelerationConstraint getAccelerationConstraint(double maxAccel) { + return new ProfileAccelerationConstraint(maxAccel); + } + + public String getTelemetry() { + return "Drive: "+getPoseEstimate(); + } + + public void setInput(Gamepad gamepad1, Gamepad gamepad2) { + double x = gamepad1.left_stick_x; + double y = -gamepad1.left_stick_y; + double z = gamepad1.right_stick_x; + + setInput(gamepad1, gamepad2); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleTankDrive.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleTankDrive.java new file mode 100644 index 0000000..63852f8 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/SampleTankDrive.java @@ -0,0 +1,321 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive; + +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_ACCEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_ANG_ACCEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_ANG_VEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_VEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MOTOR_VELO_PID; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.RUN_USING_ENCODER; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.TRACK_WIDTH; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.encoderTicksToInches; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kA; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kStatic; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kV; + +import androidx.annotation.NonNull; + +import com.acmerobotics.roadrunner.control.PIDCoefficients; +import com.acmerobotics.roadrunner.drive.DriveSignal; +import com.acmerobotics.roadrunner.drive.TankDrive; +import com.acmerobotics.roadrunner.followers.TankPIDVAFollower; +import com.acmerobotics.roadrunner.followers.TrajectoryFollower; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.acmerobotics.roadrunner.trajectory.TrajectoryBuilder; +import com.acmerobotics.roadrunner.trajectory.constraints.AngularVelocityConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.MinVelocityConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.ProfileAccelerationConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.TankVelocityConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryAccelerationConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryVelocityConstraint; +import com.qualcomm.hardware.bosch.BNO055IMU; +import com.qualcomm.hardware.lynx.LynxModule; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.DcMotorEx; +import com.qualcomm.robotcore.hardware.HardwareMap; +import com.qualcomm.robotcore.hardware.PIDFCoefficients; +import com.qualcomm.robotcore.hardware.VoltageSensor; +import com.qualcomm.robotcore.hardware.configuration.typecontainers.MotorConfigurationType; + +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequence; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequenceBuilder; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequenceRunner; +import org.firstinspires.ftc.teamcode.roadrunner.util.LynxModuleUtil; + +import java.util.Arrays; +import java.util.List; + +/* + * Simple tank drive hardware implementation for REV hardware. + */ +public class SampleTankDrive extends TankDrive { + public static PIDCoefficients AXIAL_PID = new PIDCoefficients(0, 0, 0); + public static PIDCoefficients CROSS_TRACK_PID = new PIDCoefficients(0, 0, 0); + public static PIDCoefficients HEADING_PID = new PIDCoefficients(0, 0, 0); + + public static double VX_WEIGHT = 1; + public static double OMEGA_WEIGHT = 1; + + private TrajectorySequenceRunner trajectorySequenceRunner; + + private static final TrajectoryVelocityConstraint VEL_CONSTRAINT = getVelocityConstraint(MAX_VEL, MAX_ANG_VEL, TRACK_WIDTH); + private static final TrajectoryAccelerationConstraint accelConstraint = getAccelerationConstraint(MAX_ACCEL); + + private TrajectoryFollower follower; + + private List motors, leftMotors, rightMotors; + private BNO055IMU imu; + + private VoltageSensor batteryVoltageSensor; + + public SampleTankDrive(HardwareMap hardwareMap) { + super(kV, kA, kStatic, TRACK_WIDTH); + + follower = new TankPIDVAFollower(AXIAL_PID, CROSS_TRACK_PID, + new Pose2d(0.5, 0.5, Math.toRadians(5.0)), 0.5); + + LynxModuleUtil.ensureMinimumFirmwareVersion(hardwareMap); + + batteryVoltageSensor = hardwareMap.voltageSensor.iterator().next(); + + for (LynxModule module : hardwareMap.getAll(LynxModule.class)) { + module.setBulkCachingMode(LynxModule.BulkCachingMode.AUTO); + } + + // TODO: adjust the names of the following hardware devices to match your configuration + imu = hardwareMap.get(BNO055IMU.class, "imu"); + BNO055IMU.Parameters parameters = new BNO055IMU.Parameters(); + parameters.angleUnit = BNO055IMU.AngleUnit.RADIANS; + imu.initialize(parameters); + + // TODO: If the hub containing the IMU you are using is mounted so that the "REV" logo does + // not face up, remap the IMU axes so that the z-axis points upward (normal to the floor.) + // + // | +Z axis + // | + // | + // | + // _______|_____________ +Y axis + // / |_____________/|__________ + // / REV / EXPANSION // + // / / HUB // + // /_______/_____________// + // |_______/_____________|/ + // / + // / +X axis + // + // This diagram is derived from the axes in section 3.4 https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bno055-ds000.pdf + // and the placement of the dot/orientation from https://docs.revrobotics.com/rev-control-system/control-system-overview/dimensions#imu-location + // + // For example, if +Y in this diagram faces downwards, you would use AxisDirection.NEG_Y. + // BNO055IMUUtil.remapZAxis(imu, AxisDirection.NEG_Y); + + // add/remove motors depending on your robot (e.g., 6WD) + DcMotorEx leftFront = hardwareMap.get(DcMotorEx.class, "leftFront"); + DcMotorEx leftRear = hardwareMap.get(DcMotorEx.class, "leftRear"); + DcMotorEx rightRear = hardwareMap.get(DcMotorEx.class, "rightRear"); + DcMotorEx rightFront = hardwareMap.get(DcMotorEx.class, "rightFront"); + + motors = Arrays.asList(leftFront, leftRear, rightRear, rightFront); + leftMotors = Arrays.asList(leftFront, leftRear); + rightMotors = Arrays.asList(rightFront, rightRear); + + for (DcMotorEx motor : motors) { + MotorConfigurationType motorConfigurationType = motor.getMotorType().clone(); + motorConfigurationType.setAchieveableMaxRPMFraction(1.0); + motor.setMotorType(motorConfigurationType); + } + + if (RUN_USING_ENCODER) { + setMode(DcMotor.RunMode.RUN_USING_ENCODER); + } + + setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + + if (RUN_USING_ENCODER && MOTOR_VELO_PID != null) { + setPIDFCoefficients(DcMotor.RunMode.RUN_USING_ENCODER, MOTOR_VELO_PID); + } + + // TODO: reverse any motors using DcMotor.setDirection() + + // TODO: if desired, use setLocalizer() to change the localization method + // for instance, setLocalizer(new ThreeTrackingWheelLocalizer(...)); + + trajectorySequenceRunner = new TrajectorySequenceRunner(follower, HEADING_PID); + } + + public TrajectoryBuilder trajectoryBuilder(Pose2d startPose) { + return new TrajectoryBuilder(startPose, VEL_CONSTRAINT, accelConstraint); + } + + public TrajectoryBuilder trajectoryBuilder(Pose2d startPose, boolean reversed) { + return new TrajectoryBuilder(startPose, reversed, VEL_CONSTRAINT, accelConstraint); + } + + public TrajectoryBuilder trajectoryBuilder(Pose2d startPose, double startHeading) { + return new TrajectoryBuilder(startPose, startHeading, VEL_CONSTRAINT, accelConstraint); + } + + public TrajectorySequenceBuilder trajectorySequenceBuilder(Pose2d startPose) { + return new TrajectorySequenceBuilder( + startPose, + VEL_CONSTRAINT, accelConstraint, + MAX_ANG_VEL, MAX_ANG_ACCEL + ); + } + + public void turnAsync(double angle) { + trajectorySequenceRunner.followTrajectorySequenceAsync( + trajectorySequenceBuilder(getPoseEstimate()) + .turn(angle) + .build() + ); + } + + public void turn(double angle) { + turnAsync(angle); + waitForIdle(); + } + + public void followTrajectoryAsync(Trajectory trajectory) { + trajectorySequenceRunner.followTrajectorySequenceAsync( + trajectorySequenceBuilder(trajectory.start()) + .addTrajectory(trajectory) + .build() + ); + } + + public void followTrajectory(Trajectory trajectory) { + followTrajectoryAsync(trajectory); + waitForIdle(); + } + + public void followTrajectorySequenceAsync(TrajectorySequence trajectorySequence) { + trajectorySequenceRunner.followTrajectorySequenceAsync(trajectorySequence); + } + + public void followTrajectorySequence(TrajectorySequence trajectorySequence) { + followTrajectorySequenceAsync(trajectorySequence); + waitForIdle(); + } + + public Pose2d getLastError() { + return trajectorySequenceRunner.getLastPoseError(); + } + + + public void update() { + updatePoseEstimate(); + DriveSignal signal = trajectorySequenceRunner.update(getPoseEstimate(), getPoseVelocity()); + if (signal != null) setDriveSignal(signal); + } + + public void waitForIdle() { + while (!Thread.currentThread().isInterrupted() && isBusy()) + update(); + } + + public boolean isBusy() { + return trajectorySequenceRunner.isBusy(); + } + + public void setMode(DcMotor.RunMode runMode) { + for (DcMotorEx motor : motors) { + motor.setMode(runMode); + } + } + + public void setZeroPowerBehavior(DcMotor.ZeroPowerBehavior zeroPowerBehavior) { + for (DcMotorEx motor : motors) { + motor.setZeroPowerBehavior(zeroPowerBehavior); + } + } + + public void setPIDFCoefficients(DcMotor.RunMode runMode, PIDFCoefficients coefficients) { + PIDFCoefficients compensatedCoefficients = new PIDFCoefficients( + coefficients.p, coefficients.i, coefficients.d, + coefficients.f * 12 / batteryVoltageSensor.getVoltage() + ); + for (DcMotorEx motor : motors) { + motor.setPIDFCoefficients(runMode, compensatedCoefficients); + } + } + + public void setWeightedDrivePower(Pose2d drivePower) { + Pose2d vel = drivePower; + + if (Math.abs(drivePower.getX()) + Math.abs(drivePower.getHeading()) > 1) { + // re-normalize the powers according to the weights + double denom = VX_WEIGHT * Math.abs(drivePower.getX()) + + OMEGA_WEIGHT * Math.abs(drivePower.getHeading()); + + vel = new Pose2d( + VX_WEIGHT * drivePower.getX(), + 0, + OMEGA_WEIGHT * drivePower.getHeading() + ).div(denom); + } + + setDrivePower(vel); + } + + @NonNull + @Override + public List getWheelPositions() { + double leftSum = 0, rightSum = 0; + for (DcMotorEx leftMotor : leftMotors) { + leftSum += encoderTicksToInches(leftMotor.getCurrentPosition()); + } + for (DcMotorEx rightMotor : rightMotors) { + rightSum += encoderTicksToInches(rightMotor.getCurrentPosition()); + } + return Arrays.asList(leftSum / leftMotors.size(), rightSum / rightMotors.size()); + } + + public List getWheelVelocities() { + double leftSum = 0, rightSum = 0; + for (DcMotorEx leftMotor : leftMotors) { + leftSum += encoderTicksToInches(leftMotor.getVelocity()); + } + for (DcMotorEx rightMotor : rightMotors) { + rightSum += encoderTicksToInches(rightMotor.getVelocity()); + } + return Arrays.asList(leftSum / leftMotors.size(), rightSum / rightMotors.size()); + } + + @Override + public void setMotorPowers(double v, double v1) { + for (DcMotorEx leftMotor : leftMotors) { + leftMotor.setPower(v); + } + for (DcMotorEx rightMotor : rightMotors) { + rightMotor.setPower(v1); + } + } + + @Override + public double getRawExternalHeading() { + return imu.getAngularOrientation().firstAngle; + } + + @Override + public Double getExternalHeadingVelocity() { + // To work around an SDK bug, use -zRotationRate in place of xRotationRate + // and -xRotationRate in place of zRotationRate (yRotationRate behaves as + // expected). This bug does NOT affect orientation. + // + // See https://github.com/FIRST-Tech-Challenge/FtcRobotController/issues/251 for details. + return (double) -imu.getAngularVelocity().xRotationRate; + } + + public static TrajectoryVelocityConstraint getVelocityConstraint(double maxVel, double maxAngularVel, double trackWidth) { + return new MinVelocityConstraint(Arrays.asList( + new AngularVelocityConstraint(maxAngularVel), + new TankVelocityConstraint(maxVel, trackWidth) + )); + } + + public static TrajectoryAccelerationConstraint getAccelerationConstraint(double maxAccel) { + return new ProfileAccelerationConstraint(maxAccel); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/StandardTrackingWheelLocalizer.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/StandardTrackingWheelLocalizer.java new file mode 100644 index 0000000..2f7ec2f --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/StandardTrackingWheelLocalizer.java @@ -0,0 +1,82 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive; + +import androidx.annotation.NonNull; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.localization.ThreeTrackingWheelLocalizer; +import com.qualcomm.robotcore.hardware.DcMotorEx; +import com.qualcomm.robotcore.hardware.HardwareMap; + +import org.firstinspires.ftc.teamcode.roadrunner.util.Encoder; + +import java.util.Arrays; +import java.util.List; + +/* + * Sample tracking wheel localizer implementation assuming the standard configuration: + * + * /--------------\ + * | ____ | + * | ---- | + * | || || | + * | || || | + * | | + * | | + * \--------------/ + * + */ +public class StandardTrackingWheelLocalizer extends ThreeTrackingWheelLocalizer { + public static double TICKS_PER_REV = 8192; + public static double WHEEL_RADIUS = 0.6889764; // in + public static double GEAR_RATIO = 1; // output (wheel) speed / input (encoder) speed + + public static double LATERAL_DISTANCE = 11.686023622; // in; distance between the left and right wheels + public static double FORWARD_OFFSET = 4.89812992126; // in; offset of the lateral wheel + + private Encoder leftEncoder, rightEncoder, frontEncoder; + + public StandardTrackingWheelLocalizer(HardwareMap hardwareMap) { + super(Arrays.asList( + new Pose2d(0, LATERAL_DISTANCE / 2, 0), // left + new Pose2d(0, -LATERAL_DISTANCE / 2, 0), // right + new Pose2d(FORWARD_OFFSET, 0, Math.toRadians(90)) // front + )); + + leftEncoder = new Encoder(hardwareMap.get(DcMotorEx.class, "backRight")); + rightEncoder = new Encoder(hardwareMap.get(DcMotorEx.class, "frontLeft")); + frontEncoder = new Encoder(hardwareMap.get(DcMotorEx.class, "frontRight")); + + // TODO: reverse any encoders using Encoder.setDirection(Encoder.Direction.REVERSE) + leftEncoder.setDirection(Encoder.Direction.REVERSE); +// rightEncoder.setDirection(Encoder.Direction.REVERSE); +// frontEncoder.setDirection(Encoder.Direction.REVERSE); + } + + public static double encoderTicksToInches(double ticks) { + return WHEEL_RADIUS * 2 * Math.PI * GEAR_RATIO * ticks / TICKS_PER_REV; + } + + @NonNull + @Override + public List getWheelPositions() { + return Arrays.asList( + encoderTicksToInches(leftEncoder.getCurrentPosition()), + encoderTicksToInches(rightEncoder.getCurrentPosition()), + encoderTicksToInches(frontEncoder.getCurrentPosition()) + ); + } + + @NonNull + @Override + public List getWheelVelocities() { + // TODO: If your encoder velocity can exceed 32767 counts / second (such as the REV Through Bore and other + // competing magnetic encoders), change Encoder.getRawVelocity() to Encoder.getCorrectedVelocity() to enable a + // compensation method + + return Arrays.asList( + encoderTicksToInches(leftEncoder.getCorrectedVelocity()), + encoderTicksToInches(rightEncoder.getCorrectedVelocity()), + encoderTicksToInches(frontEncoder.getCorrectedVelocity()) + ); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/TwoWheelTrackingLocalizer.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/TwoWheelTrackingLocalizer.java new file mode 100644 index 0000000..5ea5411 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/TwoWheelTrackingLocalizer.java @@ -0,0 +1,108 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive; + +import androidx.annotation.NonNull; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.localization.TwoTrackingWheelLocalizer; +import com.qualcomm.robotcore.hardware.DcMotorEx; +import com.qualcomm.robotcore.hardware.HardwareMap; + +import org.firstinspires.ftc.teamcode.roadrunner.util.Encoder; + +import java.util.Arrays; +import java.util.List; + +/* + * Sample tracking wheel localizer implementation assuming the standard configuration: + * + * ^ + * | + * | ( x direction) + * | + * v + * <----( y direction )----> + * (forward) + * /--------------\ + * | ____ | + * | ---- | <- Perpendicular Wheel + * | || | + * | || | <- Parallel Wheel + * | | + * | | + * \--------------/ + * + */ +public class TwoWheelTrackingLocalizer extends TwoTrackingWheelLocalizer { + public static double TICKS_PER_REV = 8192; + public static double WHEEL_RADIUS = 0.6889764; // in + public static double GEAR_RATIO = 1; // output (wheel) speed / input (encoder) speed + + public static double PARALLEL_X = -4.48818897638; // X is the up and down direction + public static double PARALLEL_Y = 3.10679133858; // Y is the strafe direction + + public static double PERPENDICULAR_X = 2.21801181102; + public static double PERPENDICULAR_Y = 0; + + public static double MULTIPLIER_X = 1.4; + public static double MULTIPLIER_Y = 1.4; + + // Parallel/Perpendicular to the forward axis + // Parallel wheel is parallel to the forward axis + // Perpendicular is perpendicular to the forward axis + private Encoder parallelEncoder, perpendicularEncoder; + + private SampleMecanumDrive drive; + + public TwoWheelTrackingLocalizer(HardwareMap hardwareMap, SampleMecanumDrive drive) { + super(Arrays.asList( + new Pose2d(PARALLEL_X, PARALLEL_Y, 0), + new Pose2d(PERPENDICULAR_X, PERPENDICULAR_Y, Math.toRadians(90)) + )); + + this.drive = drive; + + parallelEncoder = new Encoder(hardwareMap.get(DcMotorEx.class, "parallel")); +// parallelEncoder = new Encoder(hardwareMap.get(DcMotorEx.class, "backRight")); + perpendicularEncoder = new Encoder(hardwareMap.get(DcMotorEx.class, "perpendicular")); + + // TODO: reverse any encoders using Encoder.setDirection(Encoder.Direction.REVERSE) + perpendicularEncoder.setDirection(Encoder.Direction.REVERSE); + parallelEncoder.setDirection(Encoder.Direction.REVERSE); + } + + public static double encoderTicksToInches(double ticks) { + return WHEEL_RADIUS * 2 * Math.PI * GEAR_RATIO * ticks / TICKS_PER_REV; + } + + @Override + public double getHeading() { + return drive.getRawExternalHeading(); + } + + @Override + public Double getHeadingVelocity() { + return drive.getExternalHeadingVelocity(); + } + + @NonNull + @Override + public List getWheelPositions() { + return Arrays.asList( + encoderTicksToInches(parallelEncoder.getCurrentPosition()), + encoderTicksToInches(perpendicularEncoder.getCurrentPosition()) + ); + } + + @NonNull + @Override + public List getWheelVelocities() { + // TODO: If your encoder velocity can exceed 32767 counts / second (such as the REV Through Bore and other + // competing magnetic encoders), change Encoder.getRawVelocity() to Encoder.getCorrectedVelocity() to enable a + // compensation method + + return Arrays.asList( + encoderTicksToInches(parallelEncoder.getCorrectedVelocity()), + encoderTicksToInches(perpendicularEncoder.getCorrectedVelocity()) + ); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/AutomaticFeedforwardTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/AutomaticFeedforwardTuner.java new file mode 100644 index 0000000..e275220 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/AutomaticFeedforwardTuner.java @@ -0,0 +1,203 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_RPM; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.RUN_USING_ENCODER; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.rpmToVelocity; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.util.NanoClock; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; +import org.firstinspires.ftc.teamcode.roadrunner.util.LoggingUtil; + +import java.util.ArrayList; +import java.util.List; + +/* + * Op mode for computing kV, kStatic, and kA from various drive routines. For the curious, here's an + * outline of the procedure: + * 1. Slowly ramp the motor power and record encoder values along the way. + * 2. Run a linear regression on the encoder velocity vs. motor power plot to obtain a slope (kV) + * and an optional intercept (kStatic). + * 3. Accelerate the robot (apply constant power) and record the encoder counts. + * 4. Adjust the encoder data based on the velocity tuning data and find kA with another linear + * regression. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class AutomaticFeedforwardTuner extends LinearOpMode { + public static double MAX_POWER = 0.7; + public static double DISTANCE = 100; // in + + @Override + public void runOpMode() throws InterruptedException { + if (RUN_USING_ENCODER) { + RobotLog.setGlobalErrorMsg("Feedforward constants usually don't need to be tuned " + + "when using the built-in drive motor velocity PID."); + } + + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + NanoClock clock = NanoClock.system(); + + telemetry.addLine("Press play to begin the feedforward tuning routine"); + telemetry.update(); + + waitForStart(); + + if (isStopRequested()) return; + + telemetry.clearAll(); + telemetry.addLine("Would you like to fit kStatic?"); + telemetry.addLine("Press (Y/Δ) for yes, (B/O) for no"); + telemetry.update(); + + boolean fitIntercept = false; + while (!isStopRequested()) { + if (gamepad1.y) { + fitIntercept = true; + while (!isStopRequested() && gamepad1.y) { + idle(); + } + break; + } else if (gamepad1.b) { + while (!isStopRequested() && gamepad1.b) { + idle(); + } + break; + } + idle(); + } + + telemetry.clearAll(); + telemetry.addLine(Misc.formatInvariant( + "Place your robot on the field with at least %.2f in of room in front", DISTANCE)); + telemetry.addLine("Press (Y/Δ) to begin"); + telemetry.update(); + + while (!isStopRequested() && !gamepad1.y) { + idle(); + } + while (!isStopRequested() && gamepad1.y) { + idle(); + } + + telemetry.clearAll(); + telemetry.addLine("Running..."); + telemetry.update(); + + double maxVel = rpmToVelocity(MAX_RPM); + double finalVel = MAX_POWER * maxVel; + double accel = (finalVel * finalVel) / (2.0 * DISTANCE); + double rampTime = Math.sqrt(2.0 * DISTANCE / accel); + + List timeSamples = new ArrayList<>(); + List positionSamples = new ArrayList<>(); + List powerSamples = new ArrayList<>(); + + drive.setPoseEstimate(new Pose2d()); + + double startTime = clock.seconds(); + while (!isStopRequested()) { + double elapsedTime = clock.seconds() - startTime; + if (elapsedTime > rampTime) { + break; + } + double vel = accel * elapsedTime; + double power = vel / maxVel; + + timeSamples.add(elapsedTime); + positionSamples.add(drive.getPoseEstimate().getX()); + powerSamples.add(power); + + drive.setDrivePower(new Pose2d(power, 0.0, 0.0)); + drive.updatePoseEstimate(); + } + drive.setDrivePower(new Pose2d(0.0, 0.0, 0.0)); + + telemetry.clearAll(); + telemetry.addLine("Quasi-static ramp up test complete"); + telemetry.addLine("Would you like to fit kA?"); + telemetry.addLine("Press (Y/Δ) for yes, (B/O) for no"); + telemetry.update(); + + boolean fitAccelFF = false; + while (!isStopRequested()) { + if (gamepad1.y) { + fitAccelFF = true; + while (!isStopRequested() && gamepad1.y) { + idle(); + } + break; + } else if (gamepad1.b) { + while (!isStopRequested() && gamepad1.b) { + idle(); + } + break; + } + idle(); + } + + if (fitAccelFF) { + telemetry.clearAll(); + telemetry.addLine("Place the robot back in its starting position"); + telemetry.addLine("Press (Y/Δ) to continue"); + telemetry.update(); + + while (!isStopRequested() && !gamepad1.y) { + idle(); + } + while (!isStopRequested() && gamepad1.y) { + idle(); + } + + telemetry.clearAll(); + telemetry.addLine("Running..."); + telemetry.update(); + + double maxPowerTime = DISTANCE / maxVel; + + timeSamples.clear(); + positionSamples.clear(); + powerSamples.clear(); + + drive.setPoseEstimate(new Pose2d()); + drive.setDrivePower(new Pose2d(MAX_POWER, 0.0, 0.0)); + + startTime = clock.seconds(); + while (!isStopRequested()) { + double elapsedTime = clock.seconds() - startTime; + if (elapsedTime > maxPowerTime) { + break; + } + + timeSamples.add(elapsedTime); + positionSamples.add(drive.getPoseEstimate().getX()); + powerSamples.add(MAX_POWER); + + drive.updatePoseEstimate(); + } + drive.setDrivePower(new Pose2d(0.0, 0.0, 0.0)); + + + telemetry.clearAll(); + telemetry.addLine("Constant power test complete"); + telemetry.addLine(Misc.formatInvariant("kA = %.5f (R^2 = %.2f)", + telemetry.update())); + } + + while (!isStopRequested()) { + idle(); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/BackAndForth.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/BackAndForth.java new file mode 100644 index 0000000..faadebc --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/BackAndForth.java @@ -0,0 +1,51 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/* + * Op mode for preliminary tuning of the follower PID coefficients (located in the drive base + * classes). The robot drives back and forth in a straight line indefinitely. Utilization of the + * dashboard is recommended for this tuning routine. To access the dashboard, connect your computer + * to the RC's WiFi network. In your browser, navigate to https://192.168.49.1:8080/dash if you're + * using the RC phone or https://192.168.43.1:8080/dash if you are using the Control Hub. Once + * you've successfully connected, start the program, and your robot will begin moving forward and + * backward. You should observe the target position (green) and your pose estimate (blue) and adjust + * your follower PID coefficients such that you follow the target position as accurately as possible. + * If you are using SampleMecanumDrive, you should be tuning TRANSLATIONAL_PID and HEADING_PID. + * If you are using SampleTankDrive, you should be tuning AXIAL_PID, CROSS_TRACK_PID, and HEADING_PID. + * These coefficients can be tuned live in dashboard. + * + * This opmode is designed as a convenient, coarse tuning for the follower PID coefficients. It + * is recommended that you use the FollowerPIDTuner opmode for further fine tuning. + */ +//@Config +@Autonomous(group = "drive") +public class BackAndForth extends LinearOpMode { + + public static double DISTANCE = 50; + + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + Trajectory trajectoryForward = drive.trajectoryBuilder(new Pose2d()) + .forward(DISTANCE) + .build(); + + Trajectory trajectoryBackward = drive.trajectoryBuilder(trajectoryForward.end()) + .back(DISTANCE) + .build(); + + waitForStart(); + + while (opModeIsActive() && !isStopRequested()) { + drive.followTrajectory(trajectoryForward); + drive.followTrajectory(trajectoryBackward); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/DriveVelocityPIDTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/DriveVelocityPIDTuner.java new file mode 100644 index 0000000..24d4ea2 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/DriveVelocityPIDTuner.java @@ -0,0 +1,171 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_ACCEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_VEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MOTOR_VELO_PID; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.RUN_USING_ENCODER; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kV; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.profile.MotionProfile; +import com.acmerobotics.roadrunner.profile.MotionProfileGenerator; +import com.acmerobotics.roadrunner.profile.MotionState; +import com.acmerobotics.roadrunner.util.NanoClock; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +import java.util.List; + +/* + * This routine is designed to tune the PID coefficients used by the REV Expansion Hubs for closed- + * loop velocity control. Although it may seem unnecessary, tuning these coefficients is just as + * important as the positional parameters. Like the other manual tuning routines, this op mode + * relies heavily upon the dashboard. To access the dashboard, connect your computer to the RC's + * WiFi network. In your browser, navigate to https://192.168.49.1:8080/dash if you're using the RC + * phone or https://192.168.43.1:8080/dash if you are using the Control Hub. Once you've successfully + * connected, start the program, and your robot will begin moving forward and backward according to + * a motion profile. Your job is to graph the velocity errors over time and adjust the PID + * coefficients (note: the tuning variable will not appear until the op mode finishes initializing). + * Once you've found a satisfactory set of gains, add them to the DriveConstants.java file under the + * MOTOR_VELO_PID field. + * + * Recommended tuning process: + * + * 1. Increase kP until any phase lag is eliminated. Concurrently increase kD as necessary to + * mitigate oscillations. + * 2. Add kI (or adjust kF) until the steady state/constant velocity plateaus are reached. + * 3. Back off kP and kD a little until the response is less oscillatory (but without lag). + * + * Pressing Y/Δ (Xbox/PS4) will pause the tuning process and enter driver override, allowing the + * user to reset the position of the bot in the event that it drifts off the path. + * Pressing B/O (Xbox/PS4) will cede control back to the tuning process. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class DriveVelocityPIDTuner extends LinearOpMode { + public static double DISTANCE = 72; // in + + enum Mode { + DRIVER_MODE, + TUNING_MODE + } + + private static MotionProfile generateProfile(boolean movingForward) { + MotionState start = new MotionState(movingForward ? 0 : DISTANCE, 0, 0, 0); + MotionState goal = new MotionState(movingForward ? DISTANCE : 0, 0, 0, 0); + return MotionProfileGenerator.generateSimpleMotionProfile(start, goal, MAX_VEL, MAX_ACCEL); + } + + @Override + public void runOpMode() { + if (!RUN_USING_ENCODER) { + RobotLog.setGlobalErrorMsg("%s does not need to be run if the built-in motor velocity" + + "PID is not in use", getClass().getSimpleName()); + } + + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + Mode mode = Mode.TUNING_MODE; + + double lastKp = MOTOR_VELO_PID.p; + double lastKi = MOTOR_VELO_PID.i; + double lastKd = MOTOR_VELO_PID.d; + double lastKf = MOTOR_VELO_PID.f; + + drive.setPIDFCoefficients(DcMotor.RunMode.RUN_USING_ENCODER, MOTOR_VELO_PID); + + NanoClock clock = NanoClock.system(); + + telemetry.addLine("Ready!"); + telemetry.update(); + telemetry.clearAll(); + + waitForStart(); + + if (isStopRequested()) return; + + boolean movingForwards = true; + MotionProfile activeProfile = generateProfile(true); + double profileStart = clock.seconds(); + + + while (!isStopRequested()) { + telemetry.addData("mode", mode); + + switch (mode) { + case TUNING_MODE: + if (gamepad1.y) { + mode = Mode.DRIVER_MODE; + drive.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); + } + + // calculate and set the motor power + double profileTime = clock.seconds() - profileStart; + + if (profileTime > activeProfile.duration()) { + // generate a new profile + movingForwards = !movingForwards; + activeProfile = generateProfile(movingForwards); + profileStart = clock.seconds(); + } + + MotionState motionState = activeProfile.get(profileTime); + double targetPower = kV * motionState.getV(); + drive.setDrivePower(new Pose2d(targetPower, 0, 0)); + + List velocities = drive.getWheelVelocities(); + + // update telemetry + telemetry.addData("targetVelocity", motionState.getV()); + for (int i = 0; i < velocities.size(); i++) { + telemetry.addData("measuredVelocity" + i, velocities.get(i)); + telemetry.addData( + "error" + i, + motionState.getV() - velocities.get(i) + ); + } + break; + case DRIVER_MODE: + if (gamepad1.b) { + drive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + mode = Mode.TUNING_MODE; + movingForwards = true; + activeProfile = generateProfile(movingForwards); + profileStart = clock.seconds(); + } + + drive.setWeightedDrivePower( + new Pose2d( + -gamepad1.left_stick_y, + -gamepad1.left_stick_x, + -gamepad1.right_stick_x + ) + ); + break; + } + + if (lastKp != MOTOR_VELO_PID.p || lastKd != MOTOR_VELO_PID.d + || lastKi != MOTOR_VELO_PID.i || lastKf != MOTOR_VELO_PID.f) { + drive.setPIDFCoefficients(DcMotor.RunMode.RUN_USING_ENCODER, MOTOR_VELO_PID); + + lastKp = MOTOR_VELO_PID.p; + lastKi = MOTOR_VELO_PID.i; + lastKd = MOTOR_VELO_PID.d; + lastKf = MOTOR_VELO_PID.f; + } + + telemetry.update(); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/FollowerPIDTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/FollowerPIDTuner.java new file mode 100644 index 0000000..e4fbfae --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/FollowerPIDTuner.java @@ -0,0 +1,56 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.TrajectorySequence; + +/* + * Op mode for preliminary tuning of the follower PID coefficients (located in the drive base + * classes). The robot drives in a DISTANCE-by-DISTANCE square indefinitely. Utilization of the + * dashboard is recommended for this tuning routine. To access the dashboard, connect your computer + * to the RC's WiFi network. In your browser, navigate to https://192.168.49.1:8080/dash if you're + * using the RC phone or https://192.168.43.1:8080/dash if you are using the Control Hub. Once + * you've successfully connected, start the program, and your robot will begin driving in a square. + * You should observe the target position (green) and your pose estimate (blue) and adjust your + * follower PID coefficients such that you follow the target position as accurately as possible. + * If you are using SampleMecanumDrive, you should be tuning TRANSLATIONAL_PID and HEADING_PID. + * If you are using SampleTankDrive, you should be tuning AXIAL_PID, CROSS_TRACK_PID, and HEADING_PID. + * These coefficients can be tuned live in dashboard. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class FollowerPIDTuner extends LinearOpMode { + public static double DISTANCE = 48; // in + + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + Pose2d startPose = new Pose2d(-DISTANCE / 2, -DISTANCE / 2, 0); + + drive.setPoseEstimate(startPose); + + waitForStart(); + + if (isStopRequested()) return; + + while (!isStopRequested()) { + TrajectorySequence trajSeq = drive.trajectorySequenceBuilder(startPose) + .forward(DISTANCE) + .turn(Math.toRadians(90)) + .forward(DISTANCE) + .turn(Math.toRadians(90)) + .forward(DISTANCE) + .turn(Math.toRadians(90)) + .forward(DISTANCE) + .turn(Math.toRadians(90)) + .build(); + drive.followTrajectorySequence(trajSeq); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/LocalizationTest.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/LocalizationTest.java new file mode 100644 index 0000000..674d9b1 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/LocalizationTest.java @@ -0,0 +1,45 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.DcMotor; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/** + * This is a simple teleop routine for testing localization. Drive the robot around like a normal + * teleop routine and make sure the robot's estimated pose matches the robot's actual pose (slight + * errors are not out of the ordinary, especially with sudden drive motions). The goal of this + * exercise is to ascertain whether the localizer has been configured properly (note: the pure + * encoder localizer heading may be significantly off if the track width has not been tuned). + */ +@TeleOp(group = "drive") +public class LocalizationTest extends LinearOpMode { + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + drive.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); + + waitForStart(); + + while (!isStopRequested()) { + drive.setWeightedDrivePower( + new Pose2d( + -gamepad1.left_stick_y, + -gamepad1.left_stick_x, + -gamepad1.right_stick_x + ) + ); + + drive.update(); + + Pose2d poseEstimate = drive.getPoseEstimate(); + telemetry.addData("x", poseEstimate.getX()); + telemetry.addData("y", poseEstimate.getY()); + telemetry.addData("heading", poseEstimate.getHeading()); + telemetry.update(); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/ManualFeedforwardTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/ManualFeedforwardTuner.java new file mode 100644 index 0000000..7b2ba18 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/ManualFeedforwardTuner.java @@ -0,0 +1,143 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_ACCEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.MAX_VEL; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kA; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kStatic; +import static org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants.kV; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.kinematics.Kinematics; +import com.acmerobotics.roadrunner.profile.MotionProfile; +import com.acmerobotics.roadrunner.profile.MotionProfileGenerator; +import com.acmerobotics.roadrunner.profile.MotionState; +import com.acmerobotics.roadrunner.util.NanoClock; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +import java.util.Objects; + +/* + * This routine is designed to tune the open-loop feedforward coefficients. Although it may seem unnecessary, + * tuning these coefficients is just as important as the positional parameters. Like the other + * manual tuning routines, this op mode relies heavily upon the dashboard. To access the dashboard, + * connect your computer to the RC's WiFi network. In your browser, navigate to + * https://192.168.49.1:8080/dash if you're using the RC phone or https://192.168.43.1:8080/dash if + * you are using the Control Hub. Once you've successfully connected, start the program, and your + * robot will begin moving forward and backward according to a motion profile. Your job is to graph + * the velocity errors over time and adjust the feedforward coefficients. Once you've found a + * satisfactory set of gains, add them to the appropriate fields in the DriveConstants.java file. + * + * Pressing Y/Δ (Xbox/PS4) will pause the tuning process and enter driver override, allowing the + * user to reset the position of the bot in the event that it drifts off the path. + * Pressing B/O (Xbox/PS4) will cede control back to the tuning process. + */ +//@Config +@Autonomous(group = "drive") +public class ManualFeedforwardTuner extends LinearOpMode { + public static double DISTANCE = 72; // in + + private FtcDashboard dashboard = FtcDashboard.getInstance(); + + private SampleMecanumDrive drive; + + enum Mode { + DRIVER_MODE, + TUNING_MODE + } + + private Mode mode; + + private static MotionProfile generateProfile(boolean movingForward) { + MotionState start = new MotionState(movingForward ? 0 : DISTANCE, 0, 0, 0); + MotionState goal = new MotionState(movingForward ? DISTANCE : 0, 0, 0, 0); + return MotionProfileGenerator.generateSimpleMotionProfile(start, goal, MAX_VEL, MAX_ACCEL); + } + + @Override + public void runOpMode() { +// if (RUN_USING_ENCODER) { +// RobotLog.setGlobalErrorMsg("Feedforward constants usually don't need to be tuned " + +// "when using the built-in drive motor velocity PID."); +// } + + telemetry = new MultipleTelemetry(telemetry, dashboard.getTelemetry()); + + drive = new SampleMecanumDrive(hardwareMap); + + mode = Mode.TUNING_MODE; + + NanoClock clock = NanoClock.system(); + + telemetry.addLine("Ready!"); + telemetry.update(); + telemetry.clearAll(); + + waitForStart(); + + if (isStopRequested()) return; + + boolean movingForwards = true; + MotionProfile activeProfile = generateProfile(true); + double profileStart = clock.seconds(); + + + while (!isStopRequested()) { + telemetry.addData("mode", mode); + + switch (mode) { + case TUNING_MODE: + if (gamepad1.y) { + mode = Mode.DRIVER_MODE; + } + + // calculate and set the motor power + double profileTime = clock.seconds() - profileStart; + + if (profileTime > activeProfile.duration()) { + // generate a new profile + movingForwards = !movingForwards; + activeProfile = generateProfile(movingForwards); + profileStart = clock.seconds(); + } + + MotionState motionState = activeProfile.get(profileTime); + double targetPower = Kinematics.calculateMotorFeedforward(motionState.getV(), motionState.getA(), kV, kA, kStatic); + + drive.setDrivePower(new Pose2d(targetPower, 0, 0)); + drive.updatePoseEstimate(); + + Pose2d poseVelo = Objects.requireNonNull(drive.getPoseVelocity(), "poseVelocity() must not be null. Ensure that the getWheelVelocities() method has been overridden in your localizer."); + double currentVelo = poseVelo.getX(); + + // update telemetry + telemetry.addData("targetVelocity", motionState.getV()); + telemetry.addData("measuredVelocity", currentVelo); + telemetry.addData("error", motionState.getV() - currentVelo); + break; + case DRIVER_MODE: + if (gamepad1.b) { + mode = Mode.TUNING_MODE; + movingForwards = true; + activeProfile = generateProfile(movingForwards); + profileStart = clock.seconds(); + } + + drive.setWeightedDrivePower( + new Pose2d( + -gamepad1.left_stick_y, + -gamepad1.left_stick_x, + -gamepad1.right_stick_x + ) + ); + break; + } + + telemetry.update(); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxAngularVeloTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxAngularVeloTuner.java new file mode 100644 index 0000000..04213a9 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxAngularVeloTuner.java @@ -0,0 +1,71 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.util.ElapsedTime; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +import java.util.Objects; + +/** + * This routine is designed to calculate the maximum angular velocity your bot can achieve under load. + *

+ * Upon pressing start, your bot will turn at max power for RUNTIME seconds. + *

+ * Further fine tuning of MAX_ANG_VEL may be desired. + */ + +//@Config +@Disabled +@Autonomous(group = "drive") +public class MaxAngularVeloTuner extends LinearOpMode { + public static double RUNTIME = 4.0; + + private ElapsedTime timer; + private double maxAngVelocity = 0.0; + + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + drive.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); + + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + telemetry.addLine("Your bot will turn at full speed for " + RUNTIME + " seconds."); + telemetry.addLine("Please ensure you have enough space cleared."); + telemetry.addLine(""); + telemetry.addLine("Press start when ready."); + telemetry.update(); + + waitForStart(); + + telemetry.clearAll(); + telemetry.update(); + + drive.setDrivePower(new Pose2d(0, 0, 1)); + timer = new ElapsedTime(); + + while (!isStopRequested() && timer.seconds() < RUNTIME) { + drive.updatePoseEstimate(); + + Pose2d poseVelo = Objects.requireNonNull(drive.getPoseVelocity(), "poseVelocity() must not be null. Ensure that the getWheelVelocities() method has been overridden in your localizer."); + + maxAngVelocity = Math.max(poseVelo.getHeading(), maxAngVelocity); + } + + drive.setDrivePower(new Pose2d()); + + telemetry.addData("Max Angular Velocity (rad)", maxAngVelocity); + telemetry.addData("Max Angular Velocity (deg)", Math.toDegrees(maxAngVelocity)); + telemetry.update(); + + while (!isStopRequested()) idle(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxVelocityTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxVelocityTuner.java new file mode 100644 index 0000000..dbd8ba2 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MaxVelocityTuner.java @@ -0,0 +1,83 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.VoltageSensor; +import com.qualcomm.robotcore.util.ElapsedTime; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +import java.util.Objects; + +/** + * This routine is designed to calculate the maximum velocity your bot can achieve under load. It + * will also calculate the effective kF value for your velocity PID. + *

+ * Upon pressing start, your bot will run at max power for RUNTIME seconds. + *

+ * Further fine tuning of kF may be desired. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class MaxVelocityTuner extends LinearOpMode { + public static double RUNTIME = 2.0; + + private ElapsedTime timer; + private double maxVelocity = 0.0; + + private VoltageSensor batteryVoltageSensor; + + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + drive.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); + + batteryVoltageSensor = hardwareMap.voltageSensor.iterator().next(); + + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + telemetry.addLine("Your bot will go at full speed for " + RUNTIME + " seconds."); + telemetry.addLine("Please ensure you have enough space cleared."); + telemetry.addLine(""); + telemetry.addLine("Press start when ready."); + telemetry.update(); + + waitForStart(); + + telemetry.clearAll(); + telemetry.update(); + + drive.setDrivePower(new Pose2d(1, 0, 0)); + timer = new ElapsedTime(); + + while (!isStopRequested() && timer.seconds() < RUNTIME) { + drive.updatePoseEstimate(); + + Pose2d poseVelo = Objects.requireNonNull(drive.getPoseVelocity(), "poseVelocity() must not be null. Ensure that the getWheelVelocities() method has been overridden in your localizer."); + + maxVelocity = Math.max(poseVelo.vec().norm(), maxVelocity); + } + + drive.setDrivePower(new Pose2d()); + + double effectiveKf = DriveConstants.getMotorVelocityF(veloInchesToTicks(maxVelocity)); + + telemetry.addData("Max Velocity", maxVelocity); + telemetry.addData("Voltage Compensated kF", effectiveKf * batteryVoltageSensor.getVoltage() / 12); + telemetry.update(); + + while (!isStopRequested() && opModeIsActive()) idle(); + } + + private double veloInchesToTicks(double inchesPerSec) { + return inchesPerSec / (2 * Math.PI * DriveConstants.WHEEL_RADIUS) / DriveConstants.GEAR_RATIO * DriveConstants.TICKS_PER_REV; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MotorDirectionDebugger.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MotorDirectionDebugger.java new file mode 100644 index 0000000..d49b45b --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/MotorDirectionDebugger.java @@ -0,0 +1,90 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/** + * This is a simple teleop routine for debugging your motor configuration. + * Pressing each of the buttons will power its respective motor. + * + * Button Mappings: + * + * Xbox/PS4 Button - Motor + * X / â–¢ - Front Left + * Y / Δ - Front Right + * B / O - Rear Right + * A / X - Rear Left + * The buttons are mapped to match the wheels spatially if you + * were to rotate the gamepad 45deg°. x/square is the front left + * ________ and each button corresponds to the wheel as you go clockwise + * / ______ \ + * ------------.-' _ '-..+ Front of Bot + * / _ ( Y ) _ \ ^ + * | ( X ) _ ( B ) | Front Left \ Front Right + * ___ '. ( A ) /| Wheel \ Wheel + * .' '. '-._____.-' .' (x/â–¢) \ (Y/Δ) + * | | | \ + * '.___.' '. | Rear Left \ Rear Right + * '. / Wheel \ Wheel + * \. .' (A/X) \ (B/O) + * \________/ + * + * Uncomment the @Disabled tag below to use this opmode. + */ +//@Config +@TeleOp(group = "drive") +public class MotorDirectionDebugger extends LinearOpMode { + public static double MOTOR_POWER = 0.7; + + @Override + public void runOpMode() throws InterruptedException { + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + telemetry.addLine("Press play to begin the debugging opmode"); + telemetry.update(); + + waitForStart(); + + if (isStopRequested()) return; + + telemetry.clearAll(); + telemetry.setDisplayFormat(Telemetry.DisplayFormat.HTML); + + while (!isStopRequested()) { + telemetry.addLine("Press each button to turn on its respective motor"); + telemetry.addLine(); + telemetry.addLine("Xbox/PS4 Button - Motor"); + telemetry.addLine("  X / â–¢         - Front Left"); + telemetry.addLine("  Y / Δ         - Front Right"); + telemetry.addLine("  B / O         - Rear  Right"); + telemetry.addLine("  A / X         - Rear  Left"); + telemetry.addLine(); + + if(gamepad1.x) { + drive.setMotorPowers(MOTOR_POWER, 0, 0, 0); + telemetry.addLine("Running Motor: Front Left"); + } else if(gamepad1.y) { + drive.setMotorPowers(0, 0, 0, MOTOR_POWER); + telemetry.addLine("Running Motor: Front Right"); + } else if(gamepad1.b) { + drive.setMotorPowers(0, 0, MOTOR_POWER, 0); + telemetry.addLine("Running Motor: Rear Right"); + } else if(gamepad1.a) { + drive.setMotorPowers(0, MOTOR_POWER, 0, 0); + telemetry.addLine("Running Motor: Rear Left"); + } else { + drive.setMotorPowers(0, 0, 0, 0); + telemetry.addLine("Running Motor: None"); + } + + telemetry.update(); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/SplineTest.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/SplineTest.java new file mode 100644 index 0000000..29b6af6 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/SplineTest.java @@ -0,0 +1,40 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.geometry.Vector2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/* + * This is an example of a more complex path to really test the tuning. + */ +@Disabled +@Autonomous(group = "drive") +public class SplineTest extends LinearOpMode { + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + waitForStart(); + + if (isStopRequested()) return; + + Trajectory traj = drive.trajectoryBuilder(new Pose2d()) + .splineTo(new Vector2d(30, 30), 0) + .build(); + + drive.followTrajectory(traj); + + sleep(2000); + + drive.followTrajectory( + drive.trajectoryBuilder(traj.end(), true) + .splineTo(new Vector2d(0, 0), Math.toRadians(180)) + .build() + ); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StrafeTest.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StrafeTest.java new file mode 100644 index 0000000..8a4ec02 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StrafeTest.java @@ -0,0 +1,46 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/* + * This is a simple routine to test translational drive capabilities. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class StrafeTest extends LinearOpMode { + public static double DISTANCE = 60; // in + + @Override + public void runOpMode() throws InterruptedException { + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + Trajectory trajectory = drive.trajectoryBuilder(new Pose2d()) + .strafeRight(DISTANCE) + .build(); + + waitForStart(); + + if (isStopRequested()) return; + + drive.followTrajectory(trajectory); + + Pose2d poseEstimate = drive.getPoseEstimate(); + telemetry.addData("finalX", poseEstimate.getX()); + telemetry.addData("finalY", poseEstimate.getY()); + telemetry.addData("finalHeading", poseEstimate.getHeading()); + telemetry.update(); + + while (!isStopRequested() && opModeIsActive()) ; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StraightTest.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StraightTest.java new file mode 100644 index 0000000..da1d7fe --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/StraightTest.java @@ -0,0 +1,44 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/* + * This is a simple routine to test translational drive capabilities. + */ +//@Config +@Autonomous(group = "drive") +public class StraightTest extends LinearOpMode { + public static double DISTANCE = 60; // in + + @Override + public void runOpMode() throws InterruptedException { + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + Trajectory trajectory = drive.trajectoryBuilder(new Pose2d()) + .forward(DISTANCE) + .build(); + + waitForStart(); + + if (isStopRequested()) return; + + drive.followTrajectory(trajectory); + + Pose2d poseEstimate = drive.getPoseEstimate(); + telemetry.addData("finalX", poseEstimate.getX()); + telemetry.addData("finalY", poseEstimate.getY()); + telemetry.addData("finalHeading", poseEstimate.getHeading()); + telemetry.update(); + + while (!isStopRequested() && opModeIsActive()) ; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackWidthTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackWidthTuner.java new file mode 100644 index 0000000..a49fbec --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackWidthTuner.java @@ -0,0 +1,88 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.util.Angle; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.util.MovingStatistics; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; +import org.firstinspires.ftc.teamcode.roadrunner.drive.DriveConstants; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/* + * This routine determines the effective track width. The procedure works by executing a point turn + * with a given angle and measuring the difference between that angle and the actual angle (as + * indicated by an external IMU/gyro, track wheels, or some other localizer). The quotient + * given angle / actual angle gives a multiplicative adjustment to the estimated track width + * (effective track width = estimated track width * given angle / actual angle). The routine repeats + * this procedure a few times and averages the values for additional accuracy. Note: a relatively + * accurate track width estimate is important or else the angular constraints will be thrown off. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class TrackWidthTuner extends LinearOpMode { + public static double ANGLE = 180; // deg + public static int NUM_TRIALS = 5; + public static int DELAY = 1000; // ms + + @Override + public void runOpMode() throws InterruptedException { + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + // TODO: if you haven't already, set the localizer to something that doesn't depend on + // drive encoders for computing the heading + + telemetry.addLine("Press play to begin the track width tuner routine"); + telemetry.addLine("Make sure your robot has enough clearance to turn smoothly"); + telemetry.update(); + + waitForStart(); + + if (isStopRequested()) return; + + telemetry.clearAll(); + telemetry.addLine("Running..."); + telemetry.update(); + + MovingStatistics trackWidthStats = new MovingStatistics(NUM_TRIALS); + for (int i = 0; i < NUM_TRIALS; i++) { + drive.setPoseEstimate(new Pose2d()); + + // it is important to handle heading wraparounds + double headingAccumulator = 0; + double lastHeading = 0; + + drive.turnAsync(Math.toRadians(ANGLE)); + + while (!isStopRequested() && drive.isBusy()) { + double heading = drive.getPoseEstimate().getHeading(); + headingAccumulator += Angle.normDelta(heading - lastHeading); + lastHeading = heading; + + drive.update(); + } + + double trackWidth = DriveConstants.TRACK_WIDTH * Math.toRadians(ANGLE) / headingAccumulator; + trackWidthStats.add(trackWidth); + + sleep(DELAY); + } + + telemetry.clearAll(); + telemetry.addLine("Tuning complete"); + telemetry.addLine(Misc.formatInvariant("Effective track width = %.2f (SE = %.3f)", + trackWidthStats.getMean(), + trackWidthStats.getStandardDeviation() / Math.sqrt(NUM_TRIALS))); + telemetry.update(); + + while (!isStopRequested()) { + idle(); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelForwardOffsetTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelForwardOffsetTuner.java new file mode 100644 index 0000000..81dfe4d --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelForwardOffsetTuner.java @@ -0,0 +1,104 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.telemetry.MultipleTelemetry; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.util.Angle; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.util.MovingStatistics; +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; +import org.firstinspires.ftc.teamcode.roadrunner.drive.StandardTrackingWheelLocalizer; + +/** + * This routine determines the effective forward offset for the lateral tracking wheel. + * The procedure executes a point turn at a given angle for a certain number of trials, + * along with a specified delay in milliseconds. The purpose of this is to track the + * change in the y position during the turn. The offset, or distance, of the lateral tracking + * wheel from the center or rotation allows the wheel to spin during a point turn, leading + * to an incorrect measurement for the y position. This creates an arc around around + * the center of rotation with an arc length of change in y and a radius equal to the forward + * offset. We can compute this offset by calculating (change in y position) / (change in heading) + * which returns the radius if the angle (change in heading) is in radians. This is based + * on the arc length formula of length = theta * radius. + * + * To run this routine, simply adjust the desired angle and specify the number of trials + * and the desired delay. Then, run the procedure. Once it finishes, it will print the + * average of all the calculated forward offsets derived from the calculation. This calculated + * forward offset is then added onto the current forward offset to produce an overall estimate + * for the forward offset. You can run this procedure as many times as necessary until a + * satisfactory result is produced. + */ +//@Config +@Disabled +@Autonomous(group="drive") +public class TrackingWheelForwardOffsetTuner extends LinearOpMode { + public static double ANGLE = 180; // deg + public static int NUM_TRIALS = 5; + public static int DELAY = 1000; // ms + + @Override + public void runOpMode() throws InterruptedException { + telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry()); + + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + if (!(drive.getLocalizer() instanceof StandardTrackingWheelLocalizer)) { + RobotLog.setGlobalErrorMsg("StandardTrackingWheelLocalizer is not being set in the " + + "drive class. Ensure that \"setLocalizer(new StandardTrackingWheelLocalizer" + + "(hardwareMap));\" is called in SampleMecanumDrive.java"); + } + + telemetry.addLine("Press play to begin the forward offset tuner"); + telemetry.addLine("Make sure your robot has enough clearance to turn smoothly"); + telemetry.update(); + + waitForStart(); + + if (isStopRequested()) return; + + telemetry.clearAll(); + telemetry.addLine("Running..."); + telemetry.update(); + + MovingStatistics forwardOffsetStats = new MovingStatistics(NUM_TRIALS); + for (int i = 0; i < NUM_TRIALS; i++) { + drive.setPoseEstimate(new Pose2d()); + + // it is important to handle heading wraparounds + double headingAccumulator = 0; + double lastHeading = 0; + + drive.turnAsync(Math.toRadians(ANGLE)); + + while (!isStopRequested() && drive.isBusy()) { + double heading = drive.getPoseEstimate().getHeading(); + headingAccumulator += Angle.norm(heading - lastHeading); + lastHeading = heading; + + drive.update(); + } + + double forwardOffset = StandardTrackingWheelLocalizer.FORWARD_OFFSET + + drive.getPoseEstimate().getY() / headingAccumulator; + forwardOffsetStats.add(forwardOffset); + + sleep(DELAY); + } + + telemetry.clearAll(); + telemetry.addLine("Tuning complete"); + telemetry.addLine(Misc.formatInvariant("Effective forward offset = %.2f (SE = %.3f)", + forwardOffsetStats.getMean(), + forwardOffsetStats.getStandardDeviation() / Math.sqrt(NUM_TRIALS))); + telemetry.update(); + + while (!isStopRequested()) { + idle(); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelLateralDistanceTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelLateralDistanceTuner.java new file mode 100644 index 0000000..d14e016 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TrackingWheelLateralDistanceTuner.java @@ -0,0 +1,131 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.util.Angle; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; +import org.firstinspires.ftc.teamcode.roadrunner.drive.StandardTrackingWheelLocalizer; + +/** + * Opmode designed to assist the user in tuning the `StandardTrackingWheelLocalizer`'s + * LATERAL_DISTANCE value. The LATERAL_DISTANCE is the center-to-center distance of the parallel + * wheels. + * + * Tuning Routine: + * + * 1. Set the LATERAL_DISTANCE value in StandardTrackingWheelLocalizer.java to the physical + * measured value. This need only be an estimated value as you will be tuning it anyways. + * + * 2. Make a mark on the bot (with a piece of tape or sharpie or however you wish) and make an + * similar mark right below the indicator on your bot. This will be your reference point to + * ensure you've turned exactly 360°. + * + * 3. Although not entirely necessary, having the bot's pose being drawn in dashbooard does help + * identify discrepancies in the LATERAL_DISTANCE value. To access the dashboard, + * connect your computer to the RC's WiFi network. In your browser, navigate to + * https://192.168.49.1:8080/dash if you're using the RC phone or https://192.168.43.1:8080/dash + * if you are using the Control Hub. + * Ensure the field is showing (select the field view in top right of the page). + * + * 4. Press play to begin the tuning routine. + * + * 5. Use the right joystick on gamepad 1 to turn the bot counterclockwise. + * + * 6. Spin the bot 10 times, counterclockwise. Make sure to keep track of these turns. + * + * 7. Once the bot has finished spinning 10 times, press A to finishing the routine. The indicators + * on the bot and on the ground you created earlier should be lined up. + * + * 8. Your effective LATERAL_DISTANCE will be given. Stick this value into your + * StandardTrackingWheelLocalizer.java class. + * + * 9. If this value is incorrect, run the routine again while adjusting the LATERAL_DISTANCE value + * yourself. Read the heading output and follow the advice stated in the note below to manually + * nudge the values yourself. + * + * Note: + * It helps to pay attention to how the pose on the field is drawn in dashboard. A blue circle with + * a line from the circumference to the center should be present, representing the bot. The line + * indicates forward. If your LATERAL_DISTANCE value is tuned currently, the pose drawn in + * dashboard should keep track with the pose of your actual bot. If the drawn bot turns slower than + * the actual bot, the LATERAL_DISTANCE should be decreased. If the drawn bot turns faster than the + * actual bot, the LATERAL_DISTANCE should be increased. + * + * If your drawn bot oscillates around a point in dashboard, don't worry. This is because the + * position of the perpendicular wheel isn't perfectly set and causes a discrepancy in the + * effective center of rotation. You can ignore this effect. The center of rotation will be offset + * slightly but your heading will still be fine. This does not affect your overall tracking + * precision. The heading should still line up. + */ +//@Config +@Disabled +@TeleOp(group = "drive") +public class TrackingWheelLateralDistanceTuner extends LinearOpMode { + public static int NUM_TURNS = 10; + + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + if (!(drive.getLocalizer() instanceof StandardTrackingWheelLocalizer)) { + RobotLog.setGlobalErrorMsg("StandardTrackingWheelLocalizer is not being set in the " + + "drive class. Ensure that \"setLocalizer(new StandardTrackingWheelLocalizer" + + "(hardwareMap));\" is called in SampleMecanumDrive.java"); + } + + telemetry.addLine("Prior to beginning the routine, please read the directions " + + "located in the comments of the opmode file."); + telemetry.addLine("Press play to begin the tuning routine."); + telemetry.addLine(""); + telemetry.addLine("Press Y/â–³ to stop the routine."); + telemetry.update(); + + waitForStart(); + + if (isStopRequested()) return; + + telemetry.clearAll(); + telemetry.update(); + + double headingAccumulator = 0; + double lastHeading = 0; + + boolean tuningFinished = false; + + while (!isStopRequested() && !tuningFinished) { + Pose2d vel = new Pose2d(0, 0, -gamepad1.right_stick_x); + drive.setDrivePower(vel); + + drive.update(); + + double heading = drive.getPoseEstimate().getHeading(); + double deltaHeading = heading - lastHeading; + + headingAccumulator += Angle.normDelta(deltaHeading); + lastHeading = heading; + + telemetry.clearAll(); + telemetry.addLine("Total Heading (deg): " + Math.toDegrees(headingAccumulator)); + telemetry.addLine("Raw Heading (deg): " + Math.toDegrees(heading)); + telemetry.addLine(); + telemetry.addLine("Press Y/â–³ to conclude routine"); + telemetry.update(); + + if (gamepad1.y) + tuningFinished = true; + } + + telemetry.clearAll(); + telemetry.addLine("Localizer's total heading: " + Math.toDegrees(headingAccumulator) + "°"); + telemetry.addLine("Effective LATERAL_DISTANCE: " + + (headingAccumulator / (NUM_TURNS * Math.PI * 2)) * StandardTrackingWheelLocalizer.LATERAL_DISTANCE); + + telemetry.update(); + + while (!isStopRequested()) idle(); + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TurnTest.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TurnTest.java new file mode 100644 index 0000000..2d80f29 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/drive/opmode/TurnTest.java @@ -0,0 +1,28 @@ +package org.firstinspires.ftc.teamcode.roadrunner.drive.opmode; + +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + +import org.firstinspires.ftc.teamcode.roadrunner.drive.SampleMecanumDrive; + +/* + * This is a simple routine to test turning capabilities. + */ +//@Config +@Disabled +@Autonomous(group = "drive") +public class TurnTest extends LinearOpMode { + public static double ANGLE = 90; // deg + + @Override + public void runOpMode() throws InterruptedException { + SampleMecanumDrive drive = new SampleMecanumDrive(hardwareMap); + + waitForStart(); + + if (isStopRequested()) return; + + drive.turn(Math.toRadians(ANGLE)); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/EmptySequenceException.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/EmptySequenceException.java new file mode 100644 index 0000000..f778388 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/EmptySequenceException.java @@ -0,0 +1,4 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence; + + +public class EmptySequenceException extends RuntimeException { } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequence.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequence.java new file mode 100644 index 0000000..8d7ebdd --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequence.java @@ -0,0 +1,44 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence; + +import com.acmerobotics.roadrunner.geometry.Pose2d; + +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.SequenceSegment; + +import java.util.Collections; +import java.util.List; + +public class TrajectorySequence { + private final List sequenceList; + + public TrajectorySequence(List sequenceList) { + if (sequenceList.size() == 0) throw new EmptySequenceException(); + + this.sequenceList = Collections.unmodifiableList(sequenceList); + } + + public Pose2d start() { + return sequenceList.get(0).getStartPose(); + } + + public Pose2d end() { + return sequenceList.get(sequenceList.size() - 1).getEndPose(); + } + + public double duration() { + double total = 0.0; + + for (SequenceSegment segment : sequenceList) { + total += segment.getDuration(); + } + + return total; + } + + public SequenceSegment get(int i) { + return sequenceList.get(i); + } + + public int size() { + return sequenceList.size(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceBuilder.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceBuilder.java new file mode 100644 index 0000000..3cf0bc5 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceBuilder.java @@ -0,0 +1,711 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.geometry.Vector2d; +import com.acmerobotics.roadrunner.path.PathContinuityViolationException; +import com.acmerobotics.roadrunner.profile.MotionProfile; +import com.acmerobotics.roadrunner.profile.MotionProfileGenerator; +import com.acmerobotics.roadrunner.profile.MotionState; +import com.acmerobotics.roadrunner.trajectory.DisplacementMarker; +import com.acmerobotics.roadrunner.trajectory.DisplacementProducer; +import com.acmerobotics.roadrunner.trajectory.MarkerCallback; +import com.acmerobotics.roadrunner.trajectory.SpatialMarker; +import com.acmerobotics.roadrunner.trajectory.TemporalMarker; +import com.acmerobotics.roadrunner.trajectory.TimeProducer; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.acmerobotics.roadrunner.trajectory.TrajectoryBuilder; +import com.acmerobotics.roadrunner.trajectory.TrajectoryMarker; +import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryAccelerationConstraint; +import com.acmerobotics.roadrunner.trajectory.constraints.TrajectoryVelocityConstraint; +import com.acmerobotics.roadrunner.util.Angle; + +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.SequenceSegment; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.TrajectorySegment; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.TurnSegment; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.WaitSegment; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TrajectorySequenceBuilder { + private final double resolution = 0.25; + + private final TrajectoryVelocityConstraint baseVelConstraint; + private final TrajectoryAccelerationConstraint baseAccelConstraint; + + private TrajectoryVelocityConstraint currentVelConstraint; + private TrajectoryAccelerationConstraint currentAccelConstraint; + + private final double baseTurnConstraintMaxAngVel; + private final double baseTurnConstraintMaxAngAccel; + + private double currentTurnConstraintMaxAngVel; + private double currentTurnConstraintMaxAngAccel; + + private final List sequenceSegments; + + private final List temporalMarkers; + private final List displacementMarkers; + private final List spatialMarkers; + + private Pose2d lastPose; + + private double tangentOffset; + + private boolean setAbsoluteTangent; + private double absoluteTangent; + + private TrajectoryBuilder currentTrajectoryBuilder; + + private double currentDuration; + private double currentDisplacement; + + private double lastDurationTraj; + private double lastDisplacementTraj; + + public TrajectorySequenceBuilder( + Pose2d startPose, + Double startTangent, + TrajectoryVelocityConstraint baseVelConstraint, + TrajectoryAccelerationConstraint baseAccelConstraint, + double baseTurnConstraintMaxAngVel, + double baseTurnConstraintMaxAngAccel + ) { + this.baseVelConstraint = baseVelConstraint; + this.baseAccelConstraint = baseAccelConstraint; + + this.currentVelConstraint = baseVelConstraint; + this.currentAccelConstraint = baseAccelConstraint; + + this.baseTurnConstraintMaxAngVel = baseTurnConstraintMaxAngVel; + this.baseTurnConstraintMaxAngAccel = baseTurnConstraintMaxAngAccel; + + this.currentTurnConstraintMaxAngVel = baseTurnConstraintMaxAngVel; + this.currentTurnConstraintMaxAngAccel = baseTurnConstraintMaxAngAccel; + + sequenceSegments = new ArrayList<>(); + + temporalMarkers = new ArrayList<>(); + displacementMarkers = new ArrayList<>(); + spatialMarkers = new ArrayList<>(); + + lastPose = startPose; + + tangentOffset = 0.0; + + setAbsoluteTangent = (startTangent != null); + absoluteTangent = startTangent != null ? startTangent : 0.0; + + currentTrajectoryBuilder = null; + + currentDuration = 0.0; + currentDisplacement = 0.0; + + lastDurationTraj = 0.0; + lastDisplacementTraj = 0.0; + } + + public TrajectorySequenceBuilder( + Pose2d startPose, + TrajectoryVelocityConstraint baseVelConstraint, + TrajectoryAccelerationConstraint baseAccelConstraint, + double baseTurnConstraintMaxAngVel, + double baseTurnConstraintMaxAngAccel + ) { + this( + startPose, null, + baseVelConstraint, baseAccelConstraint, + baseTurnConstraintMaxAngVel, baseTurnConstraintMaxAngAccel + ); + } + + public TrajectorySequenceBuilder lineTo(Vector2d endPosition) { + return addPath(() -> currentTrajectoryBuilder.lineTo(endPosition, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder lineTo( + Vector2d endPosition, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.lineTo(endPosition, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder lineToConstantHeading(Vector2d endPosition) { + return addPath(() -> currentTrajectoryBuilder.lineToConstantHeading(endPosition, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder lineToConstantHeading( + Vector2d endPosition, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.lineToConstantHeading(endPosition, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder lineToLinearHeading(Pose2d endPose) { + return addPath(() -> currentTrajectoryBuilder.lineToLinearHeading(endPose, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder lineToLinearHeading( + Pose2d endPose, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.lineToLinearHeading(endPose, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder lineToSplineHeading(Pose2d endPose) { + return addPath(() -> currentTrajectoryBuilder.lineToSplineHeading(endPose, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder lineToSplineHeading( + Pose2d endPose, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.lineToSplineHeading(endPose, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder strafeTo(Vector2d endPosition) { + return addPath(() -> currentTrajectoryBuilder.strafeTo(endPosition, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder strafeTo( + Vector2d endPosition, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.strafeTo(endPosition, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder forward(double distance) { + return addPath(() -> currentTrajectoryBuilder.forward(distance, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder forward( + double distance, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.forward(distance, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder back(double distance) { + return addPath(() -> currentTrajectoryBuilder.back(distance, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder back( + double distance, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.back(distance, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder strafeLeft(double distance) { + return addPath(() -> currentTrajectoryBuilder.strafeLeft(distance, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder strafeLeft( + double distance, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.strafeLeft(distance, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder strafeRight(double distance) { + return addPath(() -> currentTrajectoryBuilder.strafeRight(distance, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder strafeRight( + double distance, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.strafeRight(distance, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder splineTo(Vector2d endPosition, double endHeading) { + return addPath(() -> currentTrajectoryBuilder.splineTo(endPosition, endHeading, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder splineTo( + Vector2d endPosition, + double endHeading, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.splineTo(endPosition, endHeading, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder splineToConstantHeading(Vector2d endPosition, double endHeading) { + return addPath(() -> currentTrajectoryBuilder.splineToConstantHeading(endPosition, endHeading, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder splineToConstantHeading( + Vector2d endPosition, + double endHeading, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.splineToConstantHeading(endPosition, endHeading, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder splineToLinearHeading(Pose2d endPose, double endHeading) { + return addPath(() -> currentTrajectoryBuilder.splineToLinearHeading(endPose, endHeading, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder splineToLinearHeading( + Pose2d endPose, + double endHeading, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.splineToLinearHeading(endPose, endHeading, velConstraint, accelConstraint)); + } + + public TrajectorySequenceBuilder splineToSplineHeading(Pose2d endPose, double endHeading) { + return addPath(() -> currentTrajectoryBuilder.splineToSplineHeading(endPose, endHeading, currentVelConstraint, currentAccelConstraint)); + } + + public TrajectorySequenceBuilder splineToSplineHeading( + Pose2d endPose, + double endHeading, + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + return addPath(() -> currentTrajectoryBuilder.splineToSplineHeading(endPose, endHeading, velConstraint, accelConstraint)); + } + + private TrajectorySequenceBuilder addPath(AddPathCallback callback) { + if (currentTrajectoryBuilder == null) newPath(); + + try { + callback.run(); + } catch (PathContinuityViolationException e) { + newPath(); + callback.run(); + } + + Trajectory builtTraj = currentTrajectoryBuilder.build(); + + double durationDifference = builtTraj.duration() - lastDurationTraj; + double displacementDifference = builtTraj.getPath().length() - lastDisplacementTraj; + + lastPose = builtTraj.end(); + currentDuration += durationDifference; + currentDisplacement += displacementDifference; + + lastDurationTraj = builtTraj.duration(); + lastDisplacementTraj = builtTraj.getPath().length(); + + return this; + } + + public TrajectorySequenceBuilder setTangent(double tangent) { + setAbsoluteTangent = true; + absoluteTangent = tangent; + + pushPath(); + + return this; + } + + private TrajectorySequenceBuilder setTangentOffset(double offset) { + setAbsoluteTangent = false; + + this.tangentOffset = offset; + this.pushPath(); + + return this; + } + + public TrajectorySequenceBuilder setReversed(boolean reversed) { + return reversed ? this.setTangentOffset(Math.toRadians(180.0)) : this.setTangentOffset(0.0); + } + + public TrajectorySequenceBuilder setConstraints( + TrajectoryVelocityConstraint velConstraint, + TrajectoryAccelerationConstraint accelConstraint + ) { + this.currentVelConstraint = velConstraint; + this.currentAccelConstraint = accelConstraint; + + return this; + } + + public TrajectorySequenceBuilder resetConstraints() { + this.currentVelConstraint = this.baseVelConstraint; + this.currentAccelConstraint = this.baseAccelConstraint; + + return this; + } + + public TrajectorySequenceBuilder setVelConstraint(TrajectoryVelocityConstraint velConstraint) { + this.currentVelConstraint = velConstraint; + + return this; + } + + public TrajectorySequenceBuilder resetVelConstraint() { + this.currentVelConstraint = this.baseVelConstraint; + + return this; + } + + public TrajectorySequenceBuilder setAccelConstraint(TrajectoryAccelerationConstraint accelConstraint) { + this.currentAccelConstraint = accelConstraint; + + return this; + } + + public TrajectorySequenceBuilder resetAccelConstraint() { + this.currentAccelConstraint = this.baseAccelConstraint; + + return this; + } + + public TrajectorySequenceBuilder setTurnConstraint(double maxAngVel, double maxAngAccel) { + this.currentTurnConstraintMaxAngVel = maxAngVel; + this.currentTurnConstraintMaxAngAccel = maxAngAccel; + + return this; + } + + public TrajectorySequenceBuilder resetTurnConstraint() { + this.currentTurnConstraintMaxAngVel = baseTurnConstraintMaxAngVel; + this.currentTurnConstraintMaxAngAccel = baseTurnConstraintMaxAngAccel; + + return this; + } + + public TrajectorySequenceBuilder addTemporalMarker(MarkerCallback callback) { + return this.addTemporalMarker(currentDuration, callback); + } + + public TrajectorySequenceBuilder UNSTABLE_addTemporalMarkerOffset(double offset, MarkerCallback callback) { + return this.addTemporalMarker(currentDuration + offset, callback); + } + + public TrajectorySequenceBuilder addTemporalMarker(double time, MarkerCallback callback) { + return this.addTemporalMarker(0.0, time, callback); + } + + public TrajectorySequenceBuilder addTemporalMarker(double scale, double offset, MarkerCallback callback) { + return this.addTemporalMarker(time -> scale * time + offset, callback); + } + + public TrajectorySequenceBuilder addTemporalMarker(TimeProducer time, MarkerCallback callback) { + this.temporalMarkers.add(new TemporalMarker(time, callback)); + return this; + } + + public TrajectorySequenceBuilder addSpatialMarker(Vector2d point, MarkerCallback callback) { + this.spatialMarkers.add(new SpatialMarker(point, callback)); + return this; + } + + public TrajectorySequenceBuilder addDisplacementMarker(MarkerCallback callback) { + return this.addDisplacementMarker(currentDisplacement, callback); + } + + public TrajectorySequenceBuilder UNSTABLE_addDisplacementMarkerOffset(double offset, MarkerCallback callback) { + return this.addDisplacementMarker(currentDisplacement + offset, callback); + } + + public TrajectorySequenceBuilder addDisplacementMarker(double displacement, MarkerCallback callback) { + return this.addDisplacementMarker(0.0, displacement, callback); + } + + public TrajectorySequenceBuilder addDisplacementMarker(double scale, double offset, MarkerCallback callback) { + return addDisplacementMarker((displacement -> scale * displacement + offset), callback); + } + + public TrajectorySequenceBuilder addDisplacementMarker(DisplacementProducer displacement, MarkerCallback callback) { + displacementMarkers.add(new DisplacementMarker(displacement, callback)); + + return this; + } + + public TrajectorySequenceBuilder turn(double angle) { + return turn(angle, currentTurnConstraintMaxAngVel, currentTurnConstraintMaxAngAccel); + } + + public TrajectorySequenceBuilder turn(double angle, double maxAngVel, double maxAngAccel) { + pushPath(); + + MotionProfile turnProfile = MotionProfileGenerator.generateSimpleMotionProfile( + new MotionState(lastPose.getHeading(), 0.0, 0.0, 0.0), + new MotionState(lastPose.getHeading() + angle, 0.0, 0.0, 0.0), + maxAngVel, + maxAngAccel + ); + + sequenceSegments.add(new TurnSegment(lastPose, angle, turnProfile, Collections.emptyList())); + + lastPose = new Pose2d( + lastPose.getX(), lastPose.getY(), + Angle.norm(lastPose.getHeading() + angle) + ); + + currentDuration += turnProfile.duration(); + + return this; + } + + public TrajectorySequenceBuilder waitSeconds(double seconds) { + pushPath(); + sequenceSegments.add(new WaitSegment(lastPose, seconds, Collections.emptyList())); + + currentDuration += seconds; + return this; + } + + public TrajectorySequenceBuilder addTrajectory(Trajectory trajectory) { + pushPath(); + + sequenceSegments.add(new TrajectorySegment(trajectory)); + return this; + } + + private void pushPath() { + if (currentTrajectoryBuilder != null) { + Trajectory builtTraj = currentTrajectoryBuilder.build(); + sequenceSegments.add(new TrajectorySegment(builtTraj)); + } + + currentTrajectoryBuilder = null; + } + + private void newPath() { + if (currentTrajectoryBuilder != null) + pushPath(); + + lastDurationTraj = 0.0; + lastDisplacementTraj = 0.0; + + double tangent = setAbsoluteTangent ? absoluteTangent : Angle.norm(lastPose.getHeading() + tangentOffset); + + currentTrajectoryBuilder = new TrajectoryBuilder(lastPose, tangent, currentVelConstraint, currentAccelConstraint, resolution); + } + + public TrajectorySequence build() { + pushPath(); + + List globalMarkers = convertMarkersToGlobal( + sequenceSegments, + temporalMarkers, displacementMarkers, spatialMarkers + ); + + return new TrajectorySequence(projectGlobalMarkersToLocalSegments(globalMarkers, sequenceSegments)); + } + + private List convertMarkersToGlobal( + List sequenceSegments, + List temporalMarkers, + List displacementMarkers, + List spatialMarkers + ) { + ArrayList trajectoryMarkers = new ArrayList<>(); + + // Convert temporal markers + for (TemporalMarker marker : temporalMarkers) { + trajectoryMarkers.add( + new TrajectoryMarker(marker.getProducer().produce(currentDuration), marker.getCallback()) + ); + } + + // Convert displacement markers + for (DisplacementMarker marker : displacementMarkers) { + double time = displacementToTime( + sequenceSegments, + marker.getProducer().produce(currentDisplacement) + ); + + trajectoryMarkers.add( + new TrajectoryMarker( + time, + marker.getCallback() + ) + ); + } + + // Convert spatial markers + for (SpatialMarker marker : spatialMarkers) { + trajectoryMarkers.add( + new TrajectoryMarker( + pointToTime(sequenceSegments, marker.getPoint()), + marker.getCallback() + ) + ); + } + + return trajectoryMarkers; + } + + private List projectGlobalMarkersToLocalSegments(List markers, List sequenceSegments) { + if (sequenceSegments.isEmpty()) return Collections.emptyList(); + + double totalSequenceDuration = 0; + for (SequenceSegment segment : sequenceSegments) { + totalSequenceDuration += segment.getDuration(); + } + + for (TrajectoryMarker marker : markers) { + SequenceSegment segment = null; + int segmentIndex = 0; + double segmentOffsetTime = 0; + + double currentTime = 0; + for (int i = 0; i < sequenceSegments.size(); i++) { + SequenceSegment seg = sequenceSegments.get(i); + + double markerTime = Math.min(marker.getTime(), totalSequenceDuration); + + if (currentTime + seg.getDuration() >= markerTime) { + segment = seg; + segmentIndex = i; + segmentOffsetTime = markerTime - currentTime; + + break; + } else { + currentTime += seg.getDuration(); + } + } + + SequenceSegment newSegment = null; + + if (segment instanceof WaitSegment) { + List newMarkers = new ArrayList<>(segment.getMarkers()); + + newMarkers.addAll(sequenceSegments.get(segmentIndex).getMarkers()); + newMarkers.add(new TrajectoryMarker(segmentOffsetTime, marker.getCallback())); + + WaitSegment thisSegment = (WaitSegment) segment; + newSegment = new WaitSegment(thisSegment.getStartPose(), thisSegment.getDuration(), newMarkers); + } else if (segment instanceof TurnSegment) { + List newMarkers = new ArrayList<>(segment.getMarkers()); + + newMarkers.addAll(sequenceSegments.get(segmentIndex).getMarkers()); + newMarkers.add(new TrajectoryMarker(segmentOffsetTime, marker.getCallback())); + + TurnSegment thisSegment = (TurnSegment) segment; + newSegment = new TurnSegment(thisSegment.getStartPose(), thisSegment.getTotalRotation(), thisSegment.getMotionProfile(), newMarkers); + } else if (segment instanceof TrajectorySegment) { + TrajectorySegment thisSegment = (TrajectorySegment) segment; + + List newMarkers = new ArrayList<>(thisSegment.getTrajectory().getMarkers()); + newMarkers.add(new TrajectoryMarker(segmentOffsetTime, marker.getCallback())); + + newSegment = new TrajectorySegment(new Trajectory(thisSegment.getTrajectory().getPath(), thisSegment.getTrajectory().getProfile(), newMarkers)); + } + + sequenceSegments.set(segmentIndex, newSegment); + } + + return sequenceSegments; + } + + // Taken from Road Runner's TrajectoryGenerator.displacementToTime() since it's private + // note: this assumes that the profile position is monotonic increasing + private Double motionProfileDisplacementToTime(MotionProfile profile, double s) { + double tLo = 0.0; + double tHi = profile.duration(); + while (!(Math.abs(tLo - tHi) < 1e-6)) { + double tMid = 0.5 * (tLo + tHi); + if (profile.get(tMid).getX() > s) { + tHi = tMid; + } else { + tLo = tMid; + } + } + return 0.5 * (tLo + tHi); + } + + private Double displacementToTime(List sequenceSegments, double s) { + double currentTime = 0.0; + double currentDisplacement = 0.0; + + for (SequenceSegment segment : sequenceSegments) { + if (segment instanceof TrajectorySegment) { + TrajectorySegment thisSegment = (TrajectorySegment) segment; + + double segmentLength = thisSegment.getTrajectory().getPath().length(); + + if (currentDisplacement + segmentLength > s) { + double target = s - currentDisplacement; + double timeInSegment = motionProfileDisplacementToTime( + thisSegment.getTrajectory().getProfile(), + target + ); + + return currentTime + timeInSegment; + } else { + currentDisplacement += segmentLength; + currentTime += thisSegment.getTrajectory().duration(); + } + } else { + currentTime += segment.getDuration(); + } + } + + return 0.0; + } + + private Double pointToTime(List sequenceSegments, Vector2d point) { + class ComparingPoints { + private final double distanceToPoint; + private final double totalDisplacement; + private final double thisPathDisplacement; + + public ComparingPoints(double distanceToPoint, double totalDisplacement, double thisPathDisplacement) { + this.distanceToPoint = distanceToPoint; + this.totalDisplacement = totalDisplacement; + this.thisPathDisplacement = thisPathDisplacement; + } + } + + List projectedPoints = new ArrayList<>(); + + for (SequenceSegment segment : sequenceSegments) { + if (segment instanceof TrajectorySegment) { + TrajectorySegment thisSegment = (TrajectorySegment) segment; + + double displacement = thisSegment.getTrajectory().getPath().project(point, 0.25); + Vector2d projectedPoint = thisSegment.getTrajectory().getPath().get(displacement).vec(); + double distanceToPoint = point.minus(projectedPoint).norm(); + + double totalDisplacement = 0.0; + + for (ComparingPoints comparingPoint : projectedPoints) { + totalDisplacement += comparingPoint.totalDisplacement; + } + + totalDisplacement += displacement; + + projectedPoints.add(new ComparingPoints(distanceToPoint, displacement, totalDisplacement)); + } + } + + ComparingPoints closestPoint = null; + + for (ComparingPoints comparingPoint : projectedPoints) { + if (closestPoint == null) { + closestPoint = comparingPoint; + continue; + } + + if (comparingPoint.distanceToPoint < closestPoint.distanceToPoint) + closestPoint = comparingPoint; + } + + return displacementToTime(sequenceSegments, closestPoint.thisPathDisplacement); + } + + private interface AddPathCallback { + void run(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceRunner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceRunner.java new file mode 100644 index 0000000..a510e6f --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/TrajectorySequenceRunner.java @@ -0,0 +1,271 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence; + +import androidx.annotation.Nullable; + +import com.acmerobotics.dashboard.FtcDashboard; +import com.acmerobotics.dashboard.canvas.Canvas; +import com.acmerobotics.dashboard.telemetry.TelemetryPacket; +import com.acmerobotics.roadrunner.control.PIDCoefficients; +import com.acmerobotics.roadrunner.control.PIDFController; +import com.acmerobotics.roadrunner.drive.DriveSignal; +import com.acmerobotics.roadrunner.followers.TrajectoryFollower; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.profile.MotionState; +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.acmerobotics.roadrunner.trajectory.TrajectoryMarker; +import com.acmerobotics.roadrunner.util.NanoClock; + +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.SequenceSegment; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.TrajectorySegment; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.TurnSegment; +import org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment.WaitSegment; +import org.firstinspires.ftc.teamcode.roadrunner.util.DashboardUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class TrajectorySequenceRunner { + public static String COLOR_INACTIVE_TRAJECTORY = "#4caf507a"; + public static String COLOR_INACTIVE_TURN = "#7c4dff7a"; + public static String COLOR_INACTIVE_WAIT = "#dd2c007a"; + + public static String COLOR_ACTIVE_TRAJECTORY = "#4CAF50"; + public static String COLOR_ACTIVE_TURN = "#7c4dff"; + public static String COLOR_ACTIVE_WAIT = "#dd2c00"; + + public static int POSE_HISTORY_LIMIT = 100; + + private final TrajectoryFollower follower; + + private final PIDFController turnController; + + private final NanoClock clock; + + private TrajectorySequence currentTrajectorySequence; + private double currentSegmentStartTime; + private int currentSegmentIndex; + private int lastSegmentIndex; + + private Pose2d lastPoseError = new Pose2d(); + + List remainingMarkers = new ArrayList<>(); + + private final FtcDashboard dashboard; + private final LinkedList poseHistory = new LinkedList<>(); + + public TrajectorySequenceRunner(TrajectoryFollower follower, PIDCoefficients headingPIDCoefficients) { + this.follower = follower; + + turnController = new PIDFController(headingPIDCoefficients); + turnController.setInputBounds(0, 2 * Math.PI); + + clock = NanoClock.system(); + + dashboard = FtcDashboard.getInstance(); + dashboard.setTelemetryTransmissionInterval(25); + } + + public void followTrajectorySequenceAsync(TrajectorySequence trajectorySequence) { + currentTrajectorySequence = trajectorySequence; + currentSegmentStartTime = clock.seconds(); + currentSegmentIndex = 0; + lastSegmentIndex = -1; + } + + public @Nullable + DriveSignal update(Pose2d poseEstimate, Pose2d poseVelocity) { + Pose2d targetPose = null; + DriveSignal driveSignal = null; + + TelemetryPacket packet = new TelemetryPacket(); + Canvas fieldOverlay = packet.fieldOverlay(); + + SequenceSegment currentSegment = null; + + if (currentTrajectorySequence != null) { + if (currentSegmentIndex >= currentTrajectorySequence.size()) { + for (TrajectoryMarker marker : remainingMarkers) { + marker.getCallback().onMarkerReached(); + } + + remainingMarkers.clear(); + + currentTrajectorySequence = null; + } + + if (currentTrajectorySequence == null) + return new DriveSignal(); + + double now = clock.seconds(); + boolean isNewTransition = currentSegmentIndex != lastSegmentIndex; + + currentSegment = currentTrajectorySequence.get(currentSegmentIndex); + + if (isNewTransition) { + currentSegmentStartTime = now; + lastSegmentIndex = currentSegmentIndex; + + for (TrajectoryMarker marker : remainingMarkers) { + marker.getCallback().onMarkerReached(); + } + + remainingMarkers.clear(); + + remainingMarkers.addAll(currentSegment.getMarkers()); + Collections.sort(remainingMarkers, (t1, t2) -> Double.compare(t1.getTime(), t2.getTime())); + } + + double deltaTime = now - currentSegmentStartTime; + + if (currentSegment instanceof TrajectorySegment) { + Trajectory currentTrajectory = ((TrajectorySegment) currentSegment).getTrajectory(); + + if (isNewTransition) + follower.followTrajectory(currentTrajectory); + + if (!follower.isFollowing()) { + currentSegmentIndex++; + + driveSignal = new DriveSignal(); + } else { + driveSignal = follower.update(poseEstimate, poseVelocity); + lastPoseError = follower.getLastError(); + } + + targetPose = currentTrajectory.get(deltaTime); + } else if (currentSegment instanceof TurnSegment) { + MotionState targetState = ((TurnSegment) currentSegment).getMotionProfile().get(deltaTime); + + turnController.setTargetPosition(targetState.getX()); + + double correction = turnController.update(poseEstimate.getHeading()); + + double targetOmega = targetState.getV(); + double targetAlpha = targetState.getA(); + + lastPoseError = new Pose2d(0, 0, turnController.getLastError()); + + Pose2d startPose = currentSegment.getStartPose(); + targetPose = startPose.copy(startPose.getX(), startPose.getY(), targetState.getX()); + + driveSignal = new DriveSignal( + new Pose2d(0, 0, targetOmega + correction), + new Pose2d(0, 0, targetAlpha) + ); + + if (deltaTime >= currentSegment.getDuration()) { + currentSegmentIndex++; + driveSignal = new DriveSignal(); + } + } else if (currentSegment instanceof WaitSegment) { + lastPoseError = new Pose2d(); + + targetPose = currentSegment.getStartPose(); + driveSignal = new DriveSignal(); + + if (deltaTime >= currentSegment.getDuration()) { + currentSegmentIndex++; + } + } + + while (remainingMarkers.size() > 0 && deltaTime > remainingMarkers.get(0).getTime()) { + remainingMarkers.get(0).getCallback().onMarkerReached(); + remainingMarkers.remove(0); + } + } + + poseHistory.add(poseEstimate); + + if (POSE_HISTORY_LIMIT > -1 && poseHistory.size() > POSE_HISTORY_LIMIT) { + poseHistory.removeFirst(); + } + + packet.put("x", poseEstimate.getX()); + packet.put("y", poseEstimate.getY()); + packet.put("heading (deg)", Math.toDegrees(poseEstimate.getHeading())); + + packet.put("xError", getLastPoseError().getX()); + packet.put("yError", getLastPoseError().getY()); + packet.put("headingError (deg)", Math.toDegrees(getLastPoseError().getHeading())); + + draw(fieldOverlay, currentTrajectorySequence, currentSegment, targetPose, poseEstimate); + + dashboard.sendTelemetryPacket(packet); + + return driveSignal; + } + + private void draw( + Canvas fieldOverlay, + TrajectorySequence sequence, SequenceSegment currentSegment, + Pose2d targetPose, Pose2d poseEstimate + ) { + if (sequence != null) { + for (int i = 0; i < sequence.size(); i++) { + SequenceSegment segment = sequence.get(i); + + if (segment instanceof TrajectorySegment) { + fieldOverlay.setStrokeWidth(1); + fieldOverlay.setStroke(COLOR_INACTIVE_TRAJECTORY); + + DashboardUtil.drawSampledPath(fieldOverlay, ((TrajectorySegment) segment).getTrajectory().getPath()); + } else if (segment instanceof TurnSegment) { + Pose2d pose = segment.getStartPose(); + + fieldOverlay.setFill(COLOR_INACTIVE_TURN); + fieldOverlay.fillCircle(pose.getX(), pose.getY(), 2); + } else if (segment instanceof WaitSegment) { + Pose2d pose = segment.getStartPose(); + + fieldOverlay.setStrokeWidth(1); + fieldOverlay.setStroke(COLOR_INACTIVE_WAIT); + fieldOverlay.strokeCircle(pose.getX(), pose.getY(), 3); + } + } + } + + if (currentSegment != null) { + if (currentSegment instanceof TrajectorySegment) { + Trajectory currentTrajectory = ((TrajectorySegment) currentSegment).getTrajectory(); + + fieldOverlay.setStrokeWidth(1); + fieldOverlay.setStroke(COLOR_ACTIVE_TRAJECTORY); + + DashboardUtil.drawSampledPath(fieldOverlay, currentTrajectory.getPath()); + } else if (currentSegment instanceof TurnSegment) { + Pose2d pose = currentSegment.getStartPose(); + + fieldOverlay.setFill(COLOR_ACTIVE_TURN); + fieldOverlay.fillCircle(pose.getX(), pose.getY(), 3); + } else if (currentSegment instanceof WaitSegment) { + Pose2d pose = currentSegment.getStartPose(); + + fieldOverlay.setStrokeWidth(1); + fieldOverlay.setStroke(COLOR_ACTIVE_WAIT); + fieldOverlay.strokeCircle(pose.getX(), pose.getY(), 3); + } + } + + if (targetPose != null) { + fieldOverlay.setStrokeWidth(1); + fieldOverlay.setStroke("#4CAF50"); + DashboardUtil.drawRobot(fieldOverlay, targetPose); + } + + fieldOverlay.setStroke("#3F51B5"); + DashboardUtil.drawPoseHistory(fieldOverlay, poseHistory); + + fieldOverlay.setStroke("#3F51B5"); + DashboardUtil.drawRobot(fieldOverlay, poseEstimate); + } + + public Pose2d getLastPoseError() { + return lastPoseError; + } + + public boolean isBusy() { + return currentTrajectorySequence != null; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/SequenceSegment.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/SequenceSegment.java new file mode 100644 index 0000000..b7ab8dd --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/SequenceSegment.java @@ -0,0 +1,40 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.TrajectoryMarker; + +import java.util.List; + +public abstract class SequenceSegment { + private final double duration; + private final Pose2d startPose; + private final Pose2d endPose; + private final List markers; + + protected SequenceSegment( + double duration, + Pose2d startPose, Pose2d endPose, + List markers + ) { + this.duration = duration; + this.startPose = startPose; + this.endPose = endPose; + this.markers = markers; + } + + public double getDuration() { + return this.duration; + } + + public Pose2d getStartPose() { + return startPose; + } + + public Pose2d getEndPose() { + return endPose; + } + + public List getMarkers() { + return markers; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TrajectorySegment.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TrajectorySegment.java new file mode 100644 index 0000000..06db735 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TrajectorySegment.java @@ -0,0 +1,20 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment; + +import com.acmerobotics.roadrunner.trajectory.Trajectory; + +import java.util.Collections; + +public final class TrajectorySegment extends SequenceSegment { + private final Trajectory trajectory; + + public TrajectorySegment(Trajectory trajectory) { + // Note: Markers are already stored in the `Trajectory` itself. + // This class should not hold any markers + super(trajectory.duration(), trajectory.start(), trajectory.end(), Collections.emptyList()); + this.trajectory = trajectory; + } + + public Trajectory getTrajectory() { + return this.trajectory; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TurnSegment.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TurnSegment.java new file mode 100644 index 0000000..7ee14ac --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/TurnSegment.java @@ -0,0 +1,36 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.profile.MotionProfile; +import com.acmerobotics.roadrunner.trajectory.TrajectoryMarker; +import com.acmerobotics.roadrunner.util.Angle; + +import java.util.List; + +public final class TurnSegment extends SequenceSegment { + private final double totalRotation; + private final MotionProfile motionProfile; + + public TurnSegment(Pose2d startPose, double totalRotation, MotionProfile motionProfile, List markers) { + super( + motionProfile.duration(), + startPose, + new Pose2d( + startPose.getX(), startPose.getY(), + Angle.norm(startPose.getHeading() + totalRotation) + ), + markers + ); + + this.totalRotation = totalRotation; + this.motionProfile = motionProfile; + } + + public final double getTotalRotation() { + return this.totalRotation; + } + + public final MotionProfile getMotionProfile() { + return this.motionProfile; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/WaitSegment.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/WaitSegment.java new file mode 100644 index 0000000..11d8e56 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/trajectorysequence/sequencesegment/WaitSegment.java @@ -0,0 +1,12 @@ +package org.firstinspires.ftc.teamcode.roadrunner.trajectorysequence.sequencesegment; + +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.trajectory.TrajectoryMarker; + +import java.util.List; + +public final class WaitSegment extends SequenceSegment { + public WaitSegment(Pose2d pose, double seconds, List markers) { + super(seconds, pose, pose, markers); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AssetsTrajectoryManager.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AssetsTrajectoryManager.java new file mode 100644 index 0000000..7135a7e --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AssetsTrajectoryManager.java @@ -0,0 +1,70 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +import androidx.annotation.Nullable; + +import com.acmerobotics.roadrunner.trajectory.Trajectory; +import com.acmerobotics.roadrunner.trajectory.TrajectoryBuilder; +import com.acmerobotics.roadrunner.trajectory.config.TrajectoryConfig; +import com.acmerobotics.roadrunner.trajectory.config.TrajectoryConfigManager; +import com.acmerobotics.roadrunner.trajectory.config.TrajectoryGroupConfig; + +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Set of utilities for loading trajectories from assets (the plugin save location). + */ +public class AssetsTrajectoryManager { + + /** + * Loads the group config. + */ + public static @Nullable + TrajectoryGroupConfig loadGroupConfig() { + try { + InputStream inputStream = AppUtil.getDefContext().getAssets().open( + "trajectory/" + TrajectoryConfigManager.GROUP_FILENAME); + return TrajectoryConfigManager.loadGroupConfig(inputStream); + } catch (IOException e) { + return null; + } + } + + /** + * Loads a trajectory config with the given name. + */ + public static @Nullable TrajectoryConfig loadConfig(String name) { + try { + InputStream inputStream = AppUtil.getDefContext().getAssets().open( + "trajectory/" + name + ".yaml"); + return TrajectoryConfigManager.loadConfig(inputStream); + } catch (IOException e) { + return null; + } + } + + /** + * Loads a trajectory builder with the given name. + */ + public static @Nullable TrajectoryBuilder loadBuilder(String name) { + TrajectoryGroupConfig groupConfig = loadGroupConfig(); + TrajectoryConfig config = loadConfig(name); + if (groupConfig == null || config == null) { + return null; + } + return config.toTrajectoryBuilder(groupConfig); + } + + /** + * Loads a trajectory with the given name. + */ + public static @Nullable Trajectory load(String name) { + TrajectoryBuilder builder = loadBuilder(name); + if (builder == null) { + return null; + } + return builder.build(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxesSigns.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxesSigns.java new file mode 100644 index 0000000..7690722 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxesSigns.java @@ -0,0 +1,45 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +/** + * IMU axes signs in the order XYZ (after remapping). + */ +public enum AxesSigns { + PPP(0b000), + PPN(0b001), + PNP(0b010), + PNN(0b011), + NPP(0b100), + NPN(0b101), + NNP(0b110), + NNN(0b111); + + public final int bVal; + + AxesSigns(int bVal) { + this.bVal = bVal; + } + + public static AxesSigns fromBinaryValue(int bVal) { + int maskedVal = bVal & 0x07; + switch (maskedVal) { + case 0b000: + return AxesSigns.PPP; + case 0b001: + return AxesSigns.PPN; + case 0b010: + return AxesSigns.PNP; + case 0b011: + return AxesSigns.PNN; + case 0b100: + return AxesSigns.NPP; + case 0b101: + return AxesSigns.NPN; + case 0b110: + return AxesSigns.NNP; + case 0b111: + return AxesSigns.NNN; + default: + throw new IllegalStateException("Unexpected value for maskedVal: " + maskedVal); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxisDirection.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxisDirection.java new file mode 100644 index 0000000..cbb9605 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/AxisDirection.java @@ -0,0 +1,8 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +/** + * A direction for an axis to be remapped to + */ +public enum AxisDirection { + POS_X, NEG_X, POS_Y, NEG_Y, POS_Z, NEG_Z +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/BNO055IMUUtil.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/BNO055IMUUtil.java new file mode 100644 index 0000000..1c76186 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/BNO055IMUUtil.java @@ -0,0 +1,128 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +import com.qualcomm.hardware.bosch.BNO055IMU; + +import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; + +/** + * Various utility functions for the BNO055 IMU. + */ +public class BNO055IMUUtil { + /** + * Error for attempting an illegal remapping (lhs or multiple same axes) + */ + public static class InvalidAxisRemapException extends RuntimeException { + public InvalidAxisRemapException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Remap BNO055 IMU axes and signs. For reference, the default order is {@link AxesOrder#XYZ}. + * Call after {@link BNO055IMU#initialize(BNO055IMU.Parameters)}. Although this isn't + * mentioned in the datasheet, the axes order appears to affect the onboard sensor fusion. + * + * Adapted from this post. + * Reference the BNO055 Datasheet for details. + * + * NOTE: Remapping axes can be somewhat confusing. Instead, use {@link #remapZAxis}, if appropriate. + * + * @param imu IMU + * @param order axes order + * @param signs axes signs + */ + public static void swapThenFlipAxes(BNO055IMU imu, AxesOrder order, AxesSigns signs) { + try { + // the indices correspond with the 2-bit axis encodings specified in the datasheet + int[] indices = order.indices(); + // AxesSign's values align with the datasheet + int axisMapSigns = signs.bVal; + + if (indices[0] == indices[1] || indices[0] == indices[2] || indices[1] == indices[2]) { + throw new InvalidAxisRemapException("Same axis cannot be included in AxesOrder twice"); + } + + // ensure right-handed coordinate system + boolean isXSwapped = indices[0] != 0; + boolean isYSwapped = indices[1] != 1; + boolean isZSwapped = indices[2] != 2; + boolean areTwoAxesSwapped = (isXSwapped || isYSwapped || isZSwapped) + && (!isXSwapped || !isYSwapped || !isZSwapped); + boolean oddNumOfFlips = (((axisMapSigns >> 2) ^ (axisMapSigns >> 1) ^ axisMapSigns) & 1) == 1; + // != functions as xor + if (areTwoAxesSwapped != oddNumOfFlips) { + throw new InvalidAxisRemapException("Coordinate system is left-handed"); + } + + // Bit: 7 6 | 5 4 | 3 2 | 1 0 | + // reserved | z axis | y axis | x axis | + int axisMapConfig = indices[2] << 4 | indices[1] << 2 | indices[0]; + + // Enter CONFIG mode + imu.write8(BNO055IMU.Register.OPR_MODE, BNO055IMU.SensorMode.CONFIG.bVal & 0x0F); + + Thread.sleep(100); + + // Write the AXIS_MAP_CONFIG register + imu.write8(BNO055IMU.Register.AXIS_MAP_CONFIG, axisMapConfig & 0x3F); + + // Write the AXIS_MAP_SIGN register + imu.write8(BNO055IMU.Register.AXIS_MAP_SIGN, axisMapSigns & 0x07); + + // Switch back to the previous mode + imu.write8(BNO055IMU.Register.OPR_MODE, imu.getParameters().mode.bVal & 0x0F); + + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Remaps the IMU coordinate system so that the remapped +Z faces the provided + * {@link AxisDirection}. See {@link #swapThenFlipAxes} for details about the remapping. + * + * @param imu IMU + * @param direction axis direction + */ + public static void remapZAxis(BNO055IMU imu, AxisDirection direction) { + switch (direction) { + case POS_X: + swapThenFlipAxes(imu, AxesOrder.ZYX, AxesSigns.NPP); + break; + case NEG_X: + swapThenFlipAxes(imu, AxesOrder.ZYX, AxesSigns.PPN); + break; + case POS_Y: + swapThenFlipAxes(imu, AxesOrder.XZY, AxesSigns.PNP); + break; + case NEG_Y: + swapThenFlipAxes(imu, AxesOrder.XZY, AxesSigns.PPN); + break; + case POS_Z: + swapThenFlipAxes(imu, AxesOrder.XYZ, AxesSigns.PPP); + break; + case NEG_Z: + swapThenFlipAxes(imu, AxesOrder.XYZ, AxesSigns.PNN); + break; + } + } + + /** + * Now deprecated due to unintuitive parameter order. + * Use {@link #swapThenFlipAxes} or {@link #remapZAxis} instead. + * + * @param imu IMU + * @param order axes order + * @param signs axes signs + */ + @Deprecated + public static void remapAxes(BNO055IMU imu, AxesOrder order, AxesSigns signs) { + AxesOrder adjustedAxesOrder = order.reverse(); + int[] indices = order.indices(); + int axisSignValue = signs.bVal ^ (0b100 >> indices[0]); + AxesSigns adjustedAxesSigns = AxesSigns.fromBinaryValue(axisSignValue); + + swapThenFlipAxes(imu, adjustedAxesOrder, adjustedAxesSigns); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/DashboardUtil.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/DashboardUtil.java new file mode 100644 index 0000000..f244b39 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/DashboardUtil.java @@ -0,0 +1,54 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +import com.acmerobotics.dashboard.canvas.Canvas; +import com.acmerobotics.roadrunner.geometry.Pose2d; +import com.acmerobotics.roadrunner.geometry.Vector2d; +import com.acmerobotics.roadrunner.path.Path; + +import java.util.List; + +/** + * Set of helper functions for drawing Road Runner paths and trajectories on dashboard canvases. + */ +public class DashboardUtil { + private static final double DEFAULT_RESOLUTION = 2.0; // distance units; presumed inches + private static final double ROBOT_RADIUS = 9; // in + + + public static void drawPoseHistory(Canvas canvas, List poseHistory) { + double[] xPoints = new double[poseHistory.size()]; + double[] yPoints = new double[poseHistory.size()]; + for (int i = 0; i < poseHistory.size(); i++) { + Pose2d pose = poseHistory.get(i); + xPoints[i] = pose.getX(); + yPoints[i] = pose.getY(); + } + canvas.strokePolyline(xPoints, yPoints); + } + + public static void drawSampledPath(Canvas canvas, Path path, double resolution) { + int samples = (int) Math.ceil(path.length() / resolution); + double[] xPoints = new double[samples]; + double[] yPoints = new double[samples]; + double dx = path.length() / (samples - 1); + for (int i = 0; i < samples; i++) { + double displacement = i * dx; + Pose2d pose = path.get(displacement); + xPoints[i] = pose.getX(); + yPoints[i] = pose.getY(); + } + canvas.strokePolyline(xPoints, yPoints); + } + + public static void drawSampledPath(Canvas canvas, Path path) { + drawSampledPath(canvas, path, DEFAULT_RESOLUTION); + } + + public static void drawRobot(Canvas canvas, Pose2d pose) { + canvas.strokeCircle(pose.getX(), pose.getY(), ROBOT_RADIUS); + Vector2d v = pose.headingVec().times(ROBOT_RADIUS); + double x1 = pose.getX() + v.getX() / 2, y1 = pose.getY() + v.getY() / 2; + double x2 = pose.getX() + v.getX(), y2 = pose.getY() + v.getY(); + canvas.strokeLine(x1, y1, x2, y2); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/Encoder.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/Encoder.java new file mode 100644 index 0000000..f3adb99 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/Encoder.java @@ -0,0 +1,98 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +import com.acmerobotics.roadrunner.util.NanoClock; +import com.qualcomm.robotcore.hardware.DcMotorEx; +import com.qualcomm.robotcore.hardware.DcMotorSimple; + +/** + * Wraps a motor instance to provide corrected velocity counts and allow reversing independently of the corresponding + * slot's motor direction + */ +public class Encoder { + private final static int CPS_STEP = 0x10000; + + private static double inverseOverflow(double input, double estimate) { + double real = input; + while (Math.abs(estimate - real) > CPS_STEP / 2.0) { + real += Math.signum(estimate - real) * CPS_STEP; + } + return real; + } + + public enum Direction { + FORWARD(1), + REVERSE(-1); + + private int multiplier; + + Direction(int multiplier) { + this.multiplier = multiplier; + } + + public int getMultiplier() { + return multiplier; + } + } + + private DcMotorEx motor; + private NanoClock clock; + + private Direction direction; + + private int lastPosition; + private double velocityEstimate; + private double lastUpdateTime; + + public Encoder(DcMotorEx motor, NanoClock clock) { + this.motor = motor; + this.clock = clock; + + this.direction = Direction.FORWARD; + + this.lastPosition = 0; + this.velocityEstimate = 0.0; + this.lastUpdateTime = clock.seconds(); + } + + public Encoder(DcMotorEx motor) { + this(motor, NanoClock.system()); + } + + public Direction getDirection() { + return direction; + } + + private int getMultiplier() { + return getDirection().getMultiplier() * (motor.getDirection() == DcMotorSimple.Direction.FORWARD ? 1 : -1); + } + + /** + * Allows you to set the direction of the counts and velocity without modifying the motor's direction state + * @param direction either reverse or forward depending on if encoder counts should be negated + */ + public void setDirection(Direction direction) { + this.direction = direction; + } + + public int getCurrentPosition() { + int multiplier = getMultiplier(); + int currentPosition = motor.getCurrentPosition() * multiplier; + if (currentPosition != lastPosition) { + double currentTime = clock.seconds(); + double dt = currentTime - lastUpdateTime; + velocityEstimate = (currentPosition - lastPosition) / dt; + lastPosition = currentPosition; + lastUpdateTime = currentTime; + } + return currentPosition; + } + + public double getRawVelocity() { + int multiplier = getMultiplier(); + return motor.getVelocity() * multiplier; + } + + public double getCorrectedVelocity() { + return inverseOverflow(getRawVelocity(), velocityEstimate); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LoggingUtil.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LoggingUtil.java new file mode 100644 index 0000000..5df601d --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LoggingUtil.java @@ -0,0 +1,60 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility functions for log files. + */ +public class LoggingUtil { + public static final File ROAD_RUNNER_FOLDER = + new File(AppUtil.ROOT_FOLDER + "/RoadRunner/"); + + private static final long LOG_QUOTA = 25 * 1024 * 1024; // 25MB log quota for now + + private static void buildLogList(List logFiles, File dir) { + for (File file : dir.listFiles()) { + if (file.isDirectory()) { + buildLogList(logFiles, file); + } else { + logFiles.add(file); + } + } + } + + private static void pruneLogsIfNecessary() { + List logFiles = new ArrayList<>(); + buildLogList(logFiles, ROAD_RUNNER_FOLDER); + Collections.sort(logFiles, (lhs, rhs) -> + Long.compare(lhs.lastModified(), rhs.lastModified())); + + long dirSize = 0; + for (File file: logFiles) { + dirSize += file.length(); + } + + while (dirSize > LOG_QUOTA) { + if (logFiles.size() == 0) break; + File fileToRemove = logFiles.remove(0); + dirSize -= fileToRemove.length(); + //noinspection ResultOfMethodCallIgnored + fileToRemove.delete(); + } + } + + /** + * Obtain a log file with the provided name + */ + public static File getLogFile(String name) { + //noinspection ResultOfMethodCallIgnored + ROAD_RUNNER_FOLDER.mkdirs(); + + pruneLogsIfNecessary(); + + return new File(ROAD_RUNNER_FOLDER, name); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LynxModuleUtil.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LynxModuleUtil.java new file mode 100644 index 0000000..1c7366c --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/roadrunner/util/LynxModuleUtil.java @@ -0,0 +1,124 @@ +package org.firstinspires.ftc.teamcode.roadrunner.util; + +import com.qualcomm.hardware.lynx.LynxModule; +import com.qualcomm.robotcore.hardware.HardwareMap; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; + +import java.util.HashMap; +import java.util.Map; + +/** + * Collection of utilites for interacting with Lynx modules. + */ +public class LynxModuleUtil { + + private static final LynxFirmwareVersion MIN_VERSION = new LynxFirmwareVersion(1, 8, 2); + + /** + * Parsed representation of a Lynx module firmware version. + */ + public static class LynxFirmwareVersion implements Comparable { + public final int major; + public final int minor; + public final int eng; + + public LynxFirmwareVersion(int major, int minor, int eng) { + this.major = major; + this.minor = minor; + this.eng = eng; + } + + @Override + public boolean equals(Object other) { + if (other instanceof LynxFirmwareVersion) { + LynxFirmwareVersion otherVersion = (LynxFirmwareVersion) other; + return major == otherVersion.major && minor == otherVersion.minor && + eng == otherVersion.eng; + } else { + return false; + } + } + + @Override + public int compareTo(LynxFirmwareVersion other) { + int majorComp = Integer.compare(major, other.major); + if (majorComp == 0) { + int minorComp = Integer.compare(minor, other.minor); + if (minorComp == 0) { + return Integer.compare(eng, other.eng); + } else { + return minorComp; + } + } else { + return majorComp; + } + } + + @Override + public String toString() { + return Misc.formatInvariant("%d.%d.%d", major, minor, eng); + } + } + + /** + * Retrieve and parse Lynx module firmware version. + * @param module Lynx module + * @return parsed firmware version + */ + public static LynxFirmwareVersion getFirmwareVersion(LynxModule module) { + String versionString = module.getNullableFirmwareVersionString(); + if (versionString == null) { + return null; + } + + String[] parts = versionString.split("[ :,]+"); + try { + // note: for now, we ignore the hardware entry + return new LynxFirmwareVersion( + Integer.parseInt(parts[3]), + Integer.parseInt(parts[5]), + Integer.parseInt(parts[7]) + ); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Exception indicating an outdated Lynx firmware version. + */ + public static class LynxFirmwareVersionException extends RuntimeException { + public LynxFirmwareVersionException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Ensure all of the Lynx modules attached to the robot satisfy the minimum requirement. + * @param hardwareMap hardware map containing Lynx modules + */ + public static void ensureMinimumFirmwareVersion(HardwareMap hardwareMap) { + Map outdatedModules = new HashMap<>(); + for (LynxModule module : hardwareMap.getAll(LynxModule.class)) { + LynxFirmwareVersion version = getFirmwareVersion(module); + if (version == null || version.compareTo(MIN_VERSION) < 0) { + for (String name : hardwareMap.getNamesOf(module)) { + outdatedModules.put(name, version); + } + } + } + if (outdatedModules.size() > 0) { + StringBuilder msgBuilder = new StringBuilder(); + msgBuilder.append("One or more of the attached Lynx modules has outdated firmware\n"); + msgBuilder.append(Misc.formatInvariant("Mandatory minimum firmware version for Road Runner: %s\n", + MIN_VERSION.toString())); + for (Map.Entry entry : outdatedModules.entrySet()) { + msgBuilder.append(Misc.formatInvariant( + "\t%s: %s\n", entry.getKey(), + entry.getValue() == null ? "Unknown" : entry.getValue().toString())); + } + throw new LynxFirmwareVersionException(msgBuilder.toString()); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/CameraPosition.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/CameraPosition.java new file mode 100644 index 0000000..0e65173 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/CameraPosition.java @@ -0,0 +1,5 @@ +package org.firstinspires.ftc.teamcode.util; + +public enum CameraPosition { + LEFT, CENTER, RIGHT +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Configurables.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Configurables.java index 50b418e..b1b0645 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Configurables.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Configurables.java @@ -41,6 +41,8 @@ public class Configurables { public static Color CAMERA_RED_POWERSHOT_UPPER = new Color(15, 255, 255); public static Color CAMERA_BLUE_GOAL_LOWER = new Color(75, 40, 80); public static Color CAMERA_BLUE_GOAL_UPPER = new Color(120, 255, 255); + public static Color CAMERA_BLACK_GOAL_LOWER = new Color(0, 0, 0); + public static Color CAMERA_BLACK_GOAL_UPPER = new Color(360, 0, 0); public static Color CAMERA_BLUE_POWERSHOT_LOWER = new Color(75, 30, 50); public static Color CAMERA_BLUE_POWERSHOT_UPPER = new Color(120, 255, 255); public static Color CAMERA_ORANGE_LOWER = new Color(0, 70, 50); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/AprilTagDetectionPipeline.java new file mode 100644 index 0000000..b11420a --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/AprilTagDetectionPipeline.java @@ -0,0 +1,194 @@ +package org.firstinspires.ftc.teamcode.vision; + +import static org.firstinspires.ftc.teamcode.vision.OpenCVUtil.LEFT_BOUNDARY_APRILTAG; +import static org.firstinspires.ftc.teamcode.vision.OpenCVUtil.RIGHT_BOUNDARY_APRILTAG; + +import org.opencv.calib3d.Calib3d; +import org.opencv.core.Mat; +import org.opencv.core.MatOfDouble; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.MatOfPoint3f; +import org.opencv.core.Point; +import org.opencv.core.Point3; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.openftc.apriltag.AprilTagDetection; +import org.openftc.apriltag.AprilTagDetectorJNI; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.util.ArrayList; + +public class AprilTagDetectionPipeline extends OpenCvPipeline { + + double fx; + double fy; + double cx; + double cy; + double tagsize; + double tagsizeX; + double tagsizeY; + Mat cameraMatrix; + private float decimation; + private Mat grey = new Mat(); + Scalar blue = new Scalar(7,197,235,255); + Scalar red = new Scalar(255,0,0,255); + Scalar green = new Scalar(0,255,0,255); + Scalar white = new Scalar(255,255,255,255); + private long nativeApriltagPtr; + private boolean needToSetDecimation; + private final Object decimationSync = new Object(); + private final Object detectionsUpdateSync = new Object(); + private ArrayList detectionsUpdate = new ArrayList<>(); + private ArrayList aprilTagDetections = new ArrayList<>(); + + @Override + public Mat processFrame(Mat input) + { + // Convert to greyscale + Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); + + synchronized (decimationSync) + { + if(needToSetDecimation) + { + AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); + needToSetDecimation = false; + } + } + + // Run AprilTag + aprilTagDetections = AprilTagDetectorJNI.runAprilTagDetectorSimple(nativeApriltagPtr, grey, tagsize, fx, fy, cx, cy); + + synchronized (detectionsUpdateSync) + { + detectionsUpdate = aprilTagDetections; + } + + for(AprilTagDetection detection : aprilTagDetections) + { + Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsizeX, tagsizeY); + drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix); + draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix); + } + Imgproc.line(input, new Point(LEFT_BOUNDARY_APRILTAG,0), new Point(LEFT_BOUNDARY_APRILTAG,800), green, 2); + Imgproc.line(input, new Point(RIGHT_BOUNDARY_APRILTAG,0), new Point(RIGHT_BOUNDARY_APRILTAG,800), green, 2); + + return input; + } + + Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsizeX , double tagsizeY) + { + // The actual 2d points of the tag detected in the image + MatOfPoint2f points2d = new MatOfPoint2f(points); + + // The 3d points of the tag in an 'ideal projection' + Point3[] arrayPoints3d = new Point3[4]; + arrayPoints3d[0] = new Point3(-tagsizeX/2, tagsizeY/2, 0); + arrayPoints3d[1] = new Point3(tagsizeX/2, tagsizeY/2, 0); + arrayPoints3d[2] = new Point3(tagsizeX/2, -tagsizeY/2, 0); + arrayPoints3d[3] = new Point3(-tagsizeX/2, -tagsizeY/2, 0); + MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); + + // Using this information, actually solve for pose + Pose pose = new Pose(); + Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false); + + return pose; + } + + void drawAxisMarker(Mat buf, double length, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) + { + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(0,0,0), + new Point3(length,0,0), + new Point3(0,length,0), + new Point3(0,0,-length) + ); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // Draw the marker! + Imgproc.line(buf, projectedPoints[0], projectedPoints[1], red, thickness); + Imgproc.line(buf, projectedPoints[0], projectedPoints[2], green, thickness); + Imgproc.line(buf, projectedPoints[0], projectedPoints[3], blue, thickness); + + Imgproc.circle(buf, projectedPoints[0], thickness, white, -1); + } + + void draw3dCubeMarker(Mat buf, double length, double tagWidth, double tagHeight, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) + { + //axis = np.float32([[0,0,0], [0,3,0], [3,3,0], [3,0,0], + // [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ]) + + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(-tagWidth/2, tagHeight/2,0), + new Point3( tagWidth/2, tagHeight/2,0), + new Point3( tagWidth/2,-tagHeight/2,0), + new Point3(-tagWidth/2,-tagHeight/2,0), + new Point3(-tagWidth/2, tagHeight/2,-length), + new Point3( tagWidth/2, tagHeight/2,-length), + new Point3( tagWidth/2,-tagHeight/2,-length), + new Point3(-tagWidth/2,-tagHeight/2,-length)); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // Pillars + for(int i = 0; i < 4; i++) + { + Imgproc.line(buf, projectedPoints[i], projectedPoints[i+4], blue, thickness); + } + + // Base lines + //Imgproc.line(buf, projectedPoints[0], projectedPoints[1], blue, thickness); + //Imgproc.line(buf, projectedPoints[1], projectedPoints[2], blue, thickness); + //Imgproc.line(buf, projectedPoints[2], projectedPoints[3], blue, thickness); + //Imgproc.line(buf, projectedPoints[3], projectedPoints[0], blue, thickness); + + // Top lines + Imgproc.line(buf, projectedPoints[4], projectedPoints[5], green, thickness); + Imgproc.line(buf, projectedPoints[5], projectedPoints[6], green, thickness); + Imgproc.line(buf, projectedPoints[6], projectedPoints[7], green, thickness); + Imgproc.line(buf, projectedPoints[4], projectedPoints[7], green, thickness); + } + + public ArrayList getLatestDetections() + { + return aprilTagDetections; + } + + public void setDecimation(float decimation) + { + synchronized (decimationSync) + { + this.decimation = decimation; + needToSetDecimation = true; + } + } + + class Pose + { + Mat rvec; + Mat tvec; + + public Pose(){ + rvec = new Mat(); + tvec = new Mat(); + } + + public Pose(Mat rvec, Mat tvec) + { + this.rvec = rvec; + this.tvec = tvec; + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/OpenCVUtil.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/OpenCVUtil.java new file mode 100644 index 0000000..b0580de --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/OpenCVUtil.java @@ -0,0 +1,109 @@ +package org.firstinspires.ftc.teamcode.vision; + +import org.firstinspires.ftc.teamcode.util.Color; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.opencv.imgproc.Moments; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class OpenCVUtil { + public static Color YELLOW_LOWER = new Color(38, 25, 0); + public static Color YELLOW_UPPER = new Color(105, 300, 300); + + public static int LEFT_BOUNDARY = -25; + public static int RIGHT_BOUNDARY = 25; + + public static int LEFT_BOUNDARY_APRILTAG = 0; + public static int RIGHT_BOUNDARY_APRILTAG = 800; + + // Draw a point + // Draw a point + public static void drawPoint(Mat img, Point point, Scalar color, int radiusPx) { + Imgproc.circle(img, point, radiusPx, color, -1); + } + + // Get the center of a contour + public static Point getCenterOfContour(MatOfPoint contour) { + Moments moments = Imgproc.moments(contour); + return new Point(moments.m10 / moments.m00, moments.m01/ moments.m00); + } + + // Get the bottom left of a contour + public static Point getBottomLeftOfContour(MatOfPoint contour) { + Rect boundingRect = Imgproc.boundingRect(contour); + return new Point(boundingRect.x, boundingRect.y+boundingRect.height); + } + + // Get the bottom right of a contour + public static Point getBottomRightOfContour(MatOfPoint contour) { + Rect boundingRect = Imgproc.boundingRect(contour); + return new Point(boundingRect.x+boundingRect.width, boundingRect.y+boundingRect.height); + } + + // Draw a contour + public static void drawContour(Mat img, MatOfPoint contour, Scalar color) { + Imgproc.drawContours(img, Collections.singletonList(contour), 0, color, 2); + } + + // Draw a convex hull around a contour + public static void drawConvexHull(Mat img, MatOfPoint contour, Scalar color) { + MatOfInt hull = new MatOfInt(); + Imgproc.convexHull(contour, hull); + Imgproc.drawContours(img, Collections.singletonList(convertIndexesToPoints(contour, hull)), 0, color, 2); + } + + // Draw a filled in convex hull around a contour + public static void fillConvexHull(Mat img, MatOfPoint contour, Scalar color) { + MatOfInt hull = new MatOfInt(); + Imgproc.convexHull(contour, hull); + Imgproc.drawContours(img, Collections.singletonList(convertIndexesToPoints(contour, hull)), 0, color, -1); + } + + // Convert indexes to points that is used in order to draw the contours + public static MatOfPoint convertIndexesToPoints(MatOfPoint contour, MatOfInt indexes) { + int[] arrIndex = indexes.toArray(); + Point[] arrContour = contour.toArray(); + Point[] arrPoints = new Point[arrIndex.length]; + + for (int i=0;i contours) { + if (contours.size() == 0) { + return null; + } + return getLargestContours(contours, 1).get(0); + } + + // Get the top largest contours + public static List getLargestContours(List contours, int numContours) { + Collections.sort(contours, (a, b) -> (int) Imgproc.contourArea(b) - (int) Imgproc.contourArea(a)); + return contours.subList(0, Math.min(numContours, contours.size())); + } + + public static void drawAngledRect(Mat img, MatOfPoint contour, Scalar color, boolean fill) { + RotatedRect rect = Imgproc.minAreaRect(new MatOfPoint2f(contour.toArray())); + Point[] vertices = new Point[4]; + rect.points(vertices); + List boxContours = new ArrayList<>(); + boxContours.add(new MatOfPoint(vertices)); + Imgproc.drawContours(img, boxContours, 0, color, fill ? -1 : 2); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/TargetingPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/TargetingPipeline.java index 16b9a38..cd773cb 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/TargetingPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/vision/TargetingPipeline.java @@ -1,16 +1,22 @@ package org.firstinspires.ftc.teamcode.vision; +import static org.firstinspires.ftc.teamcode.util.Configurables.CAMERA_BLACK_GOAL_LOWER; +import static org.firstinspires.ftc.teamcode.util.Configurables.CAMERA_BLUE_GOAL_LOWER; +import static org.firstinspires.ftc.teamcode.util.Configurables.CAMERA_BLUE_GOAL_UPPER; import static org.firstinspires.ftc.teamcode.util.Configurables.CAMERA_RED_GOAL_LOWER; import static org.firstinspires.ftc.teamcode.util.Configurables.CAMERA_RED_GOAL_UPPER; import static org.firstinspires.ftc.teamcode.util.Configurables.CV_MAX_GOAL_AREA; import static org.firstinspires.ftc.teamcode.util.Configurables.CV_MIN_GOAL_AREA; import static org.firstinspires.ftc.teamcode.util.Constants.ANCHOR; +import static org.firstinspires.ftc.teamcode.util.Constants.BLACK; +import static org.firstinspires.ftc.teamcode.util.Constants.BLUE; import static org.firstinspires.ftc.teamcode.util.Constants.BLUR_SIZE; import static org.firstinspires.ftc.teamcode.util.Constants.ERODE_DILATE_ITERATIONS; import static org.firstinspires.ftc.teamcode.util.Constants.RED; import static org.firstinspires.ftc.teamcode.util.Constants.STRUCTURING_ELEMENT; import static org.firstinspires.ftc.teamcode.util.OpenCVUtil.getLargestContour; +import org.firstinspires.ftc.teamcode.util.Color; import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.MatOfPoint; @@ -26,18 +32,31 @@ public class TargetingPipeline extends OpenCvPipeline { Mat redMask1 = new Mat(); Mat redMask2 = new Mat(); Mat redMask = new Mat(); - Mat whiteMask = new Mat(); + Mat blueMask = new Mat(); + Mat blackMask = new Mat(); Scalar redGoalLower1; Scalar redGoalUpper1; Scalar redGoalLower2; Scalar redGoalUpper2; + Scalar blueGoalLower1; + Scalar blueGoalUpper1; + Scalar blueGoalLower2; + Scalar blueGoalUpper2; + Scalar blackGoalLower1; + Scalar blackGoalUpper1; + Scalar blackGoalLower2; + Scalar blackGoalUpper2; private Detection red; + private Detection blue; + private Detection black; // Init @Override public void init(Mat input) { red = new Detection(input.size(), CV_MIN_GOAL_AREA); + blue = new Detection(input.size(), CV_MIN_GOAL_AREA); + black = new Detection(input.size(), CV_MIN_GOAL_AREA); } // Process each frame that is received from the webcam @@ -47,7 +66,8 @@ public class TargetingPipeline extends OpenCvPipeline { Imgproc.cvtColor(blurred, hsv, Imgproc.COLOR_RGB2HSV); updateRed(input); - + updateBlue(input); + updateBlack(input); return input; } @@ -76,7 +96,59 @@ public class TargetingPipeline extends OpenCvPipeline { red.fill(input, RED); } + private void updateBlue(Mat input) { + // take pixels that are in the color range and put them into a mask, eroding and dilating them to remove white noise + blueGoalLower1 = new Scalar(CAMERA_BLUE_GOAL_LOWER.getH(), CAMERA_BLUE_GOAL_LOWER.getS(), CAMERA_BLUE_GOAL_LOWER.getV()); + blueGoalUpper1 = new Scalar(180, CAMERA_BLUE_GOAL_UPPER.getS(), CAMERA_BLUE_GOAL_UPPER.getV()); + blueGoalLower2 = new Scalar(0, CAMERA_BLUE_GOAL_LOWER.getS(), CAMERA_BLUE_GOAL_LOWER.getV()); + blueGoalUpper2 = new Scalar(CAMERA_BLUE_GOAL_UPPER.getH(), CAMERA_BLUE_GOAL_UPPER.getS(), CAMERA_BLUE_GOAL_UPPER.getV()); + Core.inRange(hsv, blueGoalLower1, blueGoalUpper1, blueMask); + Core.inRange(hsv, blueGoalLower2, blueGoalUpper2, blueMask); + Imgproc.erode(blueMask, blueMask, STRUCTURING_ELEMENT, ANCHOR, ERODE_DILATE_ITERATIONS); + Imgproc.dilate(blueMask, blueMask, STRUCTURING_ELEMENT, ANCHOR, ERODE_DILATE_ITERATIONS); + + ArrayList contours = new ArrayList<>(); + Imgproc.findContours(blueMask, contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); + for (int i = 0; i < contours.size(); i++) { + Detection newDetection = new Detection(input.size(), CV_MIN_GOAL_AREA, CV_MAX_GOAL_AREA); + newDetection.setContour(contours.get(i)); + newDetection.draw(input, BLUE); + } + + blue.setContour(getLargestContour(contours)); + blue.fill(input, BLUE); + } + + private void updateBlack(Mat input) { + // take pixels that are in the color range and put them into a mask, eroding and dilating them to remove white noise + blackGoalLower1 = new Scalar(CAMERA_BLACK_GOAL_LOWER.getH(), CAMERA_BLACK_GOAL_LOWER.getS(), CAMERA_BLACK_GOAL_LOWER.getV()); + blackGoalUpper1 = new Scalar(180, CAMERA_BLACK_GOAL_LOWER.getS(), CAMERA_BLACK_GOAL_LOWER.getV()); + blackGoalLower2 = new Scalar(0, CAMERA_BLACK_GOAL_LOWER.getS(), CAMERA_BLACK_GOAL_LOWER.getV()); + blackGoalUpper2 = new Scalar(CAMERA_BLACK_GOAL_LOWER.getH(), CAMERA_BLACK_GOAL_LOWER.getS(), CAMERA_BLACK_GOAL_LOWER.getV()); + Core.inRange(hsv, blackGoalLower1, blackGoalUpper1, blackMask); + Core.inRange(hsv, blackGoalLower2, blackGoalUpper2, blackMask); + Imgproc.erode(blackMask, blackMask, STRUCTURING_ELEMENT, ANCHOR, ERODE_DILATE_ITERATIONS); + Imgproc.dilate(blackMask, blackMask, STRUCTURING_ELEMENT, ANCHOR, ERODE_DILATE_ITERATIONS); + + ArrayList contours = new ArrayList<>(); + Imgproc.findContours(blackMask, contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); + for (int i = 0; i < contours.size(); i++) { + Detection newDetection = new Detection(input.size(), CV_MIN_GOAL_AREA, CV_MAX_GOAL_AREA); + newDetection.setContour(contours.get(i)); + newDetection.draw(input, BLACK); + } + + black.setContour(getLargestContour(contours)); + black.fill(input, BLACK); + } + public Detection getRed() { return this.red; } + public Detection getBlue() { + return this.blue; + } + public Detection getBlack() { + return this.black; + } } \ No newline at end of file diff --git a/build.dependencies.gradle b/build.dependencies.gradle index 9775838..e73a9ca 100644 --- a/build.dependencies.gradle +++ b/build.dependencies.gradle @@ -1,6 +1,7 @@ repositories { mavenCentral() google() // Needed for androidx + maven { url = 'https://maven.brott.dev/' } } dependencies { @@ -17,5 +18,6 @@ dependencies { implementation 'org.tensorflow:tensorflow-lite-task-vision:0.4.3' runtimeOnly 'org.tensorflow:tensorflow-lite:2.12.0' implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.acmerobotics.dashboard:dashboard:0.4.12' }