From 4288ef6d540461b84b7c55030d836d058cecca9a Mon Sep 17 00:00:00 2001 From: henopied <13500516+henopied@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:58:37 -0500 Subject: [PATCH 1/7] Fix track width tuner angle normalization (#169) --- .../ftc/teamcode/drive/opmode/TrackWidthTuner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/drive/opmode/TrackWidthTuner.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/drive/opmode/TrackWidthTuner.java index e18b89d024..bcdaf6d133 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/drive/opmode/TrackWidthTuner.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/drive/opmode/TrackWidthTuner.java @@ -61,7 +61,7 @@ public void runOpMode() throws InterruptedException { while (!isStopRequested() && drive.isBusy()) { double heading = drive.getPoseEstimate().getHeading(); - headingAccumulator += Angle.norm(heading - lastHeading); + headingAccumulator += Angle.normDelta(heading - lastHeading); lastHeading = heading; drive.update(); From 2edaaf3981a2de56eb6b4a267fca3e3bbad6937b Mon Sep 17 00:00:00 2001 From: henopied <13500516+henopied@users.noreply.github.com> Date: Sat, 13 Aug 2022 14:50:45 -0500 Subject: [PATCH 2/7] Improve velocity compensation (#113) Fix median filtering Change ringbuffer index order Co-authored-by: Henopied --- .../ftc/teamcode/util/Encoder.java | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Encoder.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Encoder.java index c1aaaa7601..f724c5242d 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Encoder.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/Encoder.java @@ -12,10 +12,13 @@ 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; - } + // convert to uint16 + int real = (int) input & 0xffff; + // initial, modulo-based correction: it can recover the remainder of 5 of the upper 16 bits + // because the velocity is always a multiple of 20 cps due to Expansion Hub's 50ms measurement window + real += ((real % 20) / 4) * CPS_STEP; + // estimate-based correction: it finds the nearest multiple of 5 to correct the upper bits by + real += Math.round((estimate - real) / (5 * CPS_STEP)) * 5 * CPS_STEP; return real; } @@ -40,7 +43,8 @@ public int getMultiplier() { private Direction direction; private int lastPosition; - private double velocityEstimate; + private int velocityEstimateIdx; + private double[] velocityEstimates; private double lastUpdateTime; public Encoder(DcMotorEx motor, NanoClock clock) { @@ -50,7 +54,7 @@ public Encoder(DcMotorEx motor, NanoClock clock) { this.direction = Direction.FORWARD; this.lastPosition = 0; - this.velocityEstimate = 0.0; + this.velocityEstimates = new double[3]; this.lastUpdateTime = clock.seconds(); } @@ -74,25 +78,48 @@ public void setDirection(Direction direction) { this.direction = direction; } + /** + * Gets the position from the underlying motor and adjusts for the set direction. + * Additionally, this method updates the velocity estimates used for compensated velocity + * + * @return encoder position + */ 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; + velocityEstimates[velocityEstimateIdx] = (currentPosition - lastPosition) / dt; + velocityEstimateIdx = (velocityEstimateIdx + 1) % 3; lastPosition = currentPosition; lastUpdateTime = currentTime; } return currentPosition; } + /** + * Gets the velocity directly from the underlying motor and compensates for the direction + * See {@link #getCorrectedVelocity} for high (>2^15) counts per second velocities (such as on REV Through Bore) + * + * @return raw velocity + */ public double getRawVelocity() { int multiplier = getMultiplier(); return motor.getVelocity() * multiplier; } + /** + * Uses velocity estimates gathered in {@link #getCurrentPosition} to estimate the upper bits of velocity + * that are lost in overflow due to velocity being transmitted as 16 bits. + * CAVEAT: must regularly call {@link #getCurrentPosition} for the compensation to work correctly. + * + * @return corrected velocity + */ public double getCorrectedVelocity() { - return inverseOverflow(getRawVelocity(), velocityEstimate); + double median = velocityEstimates[0] > velocityEstimates[1] + ? Math.max(velocityEstimates[1], Math.min(velocityEstimates[0], velocityEstimates[2])) + : Math.max(velocityEstimates[0], Math.min(velocityEstimates[1], velocityEstimates[2])); + return inverseOverflow(getRawVelocity(), median); } } From cf1db74efd03e3ba7c43acb7e0fe25c45d3838c2 Mon Sep 17 00:00:00 2001 From: abidingabi Date: Tue, 20 Sep 2022 07:36:58 +0000 Subject: [PATCH 3/7] Merge SDK v8.0 (#170) * FtcRobotController v7.1 * FtcRobotController v7.2 * FtcRobotController v8.0 * Update FTC Dashboard to v0.4.5 Co-authored-by: Cal Kestis Co-authored-by: Cal Kestis <51927529+CalKestis@users.noreply.github.com> --- FtcRobotController/build.gradle | 1 + .../src/main/AndroidManifest.xml | 6 +- .../src/main/assets/qcar_config.xsd | 0 .../samples/BasicOmniOpMode_Linear.java | 17 +- .../samples/BasicOpMode_Iterative.java | 13 +- .../external/samples/BasicOpMode_Linear.java | 13 +- .../samples/ConceptCompassCalibration.java | 28 +- .../samples/ConceptDIMAsIndicator.java | 103 ----- .../samples/ConceptExternalHardwareClass.java | 142 ++++++ .../samples/ConceptI2cAddressChange.java | 223 --------- .../samples/ConceptMotorBulkRead.java | 23 +- .../samples/ConceptRampMotorSpeed.java | 2 +- .../external/samples/ConceptRevSPARKMini.java | 6 +- .../external/samples/ConceptScanServo.java | 6 +- .../external/samples/ConceptSoundsASJava.java | 2 +- .../samples/ConceptSoundsSKYSTONE.java | 4 +- .../ConceptTensorFlowObjectDetection.java | 79 ++-- ...rFlowObjectDetectionSwitchableCameras.java | 69 +-- ...onceptTensorFlowObjectDetectionWebcam.java | 83 ++-- .../samples/ConceptVuMarkIdentification.java | 8 +- .../ConceptVuMarkIdentificationWebcam.java | 8 +- .../ConceptVuforiaDriveToTargetWebcam.java | 21 +- .../ConceptVuforiaFieldNavigation.java | 17 +- .../ConceptVuforiaFieldNavigationWebcam.java | 12 +- .../external/samples/ConceptWebcam.java | 310 ------------- .../external/samples/HardwarePushbot.java | 105 ----- .../PushbotAutoDriveByGyro_Linear.java | 363 --------------- .../PushbotAutoDriveToLine_Linear.java | 118 ----- ...va => RobotAutoDriveByEncoder_Linear.java} | 98 ++-- .../samples/RobotAutoDriveByGyro_Linear.java | 431 ++++++++++++++++++ ....java => RobotAutoDriveByTime_Linear.java} | 61 +-- .../samples/RobotAutoDriveToLine_Linear.java | 144 ++++++ .../external/samples/RobotHardware.java | 167 +++++++ ...Linear.java => RobotTeleopPOV_Linear.java} | 71 ++- ...ve.java => RobotTeleopTank_Iterative.java} | 77 ++-- .../external/samples/SensorAdafruitRGB.java | 167 ------- .../external/samples/SensorDIO.java | 107 ----- .../external/samples/SensorMRColor.java | 2 +- .../external/samples/SensorMRGyro.java | 4 +- .../external/samples/readme.md | 20 +- .../external/samples/sample_conventions.md | 34 +- .../internal/FtcRobotControllerActivity.java | 22 +- .../src/main/res/values/strings.xml | 2 +- .../src/main/res/xml/device_filter.xml | 1 - LICENSE | 29 ++ TeamCode/build.gradle | 4 + TeamCode/src/main/AndroidManifest.xml | 1 - .../org/firstinspires/ftc/teamcode/readme.md | 48 +- build.common.gradle | 1 - build.dependencies.gradle | 22 +- build.gradle | 39 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 58695 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- 53 files changed, 1389 insertions(+), 1948 deletions(-) create mode 100644 FtcRobotController/src/main/assets/qcar_config.xsd delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptDIMAsIndicator.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExternalHardwareClass.java delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptI2cAddressChange.java delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptWebcam.java delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/HardwarePushbot.java delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByGyro_Linear.java delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveToLine_Linear.java rename FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/{PushbotAutoDriveByEncoder_Linear.java => RobotAutoDriveByEncoder_Linear.java} (62%) create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByGyro_Linear.java rename FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/{PushbotAutoDriveByTime_Linear.java => RobotAutoDriveByTime_Linear.java} (67%) create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToLine_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotHardware.java rename FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/{PushbotTeleopPOV_Linear.java => RobotTeleopPOV_Linear.java} (63%) rename FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/{PushbotTeleopTank_Iterative.java => RobotTeleopTank_Iterative.java} (56%) delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAdafruitRGB.java delete mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDIO.java create mode 100644 LICENSE diff --git a/FtcRobotController/build.gradle b/FtcRobotController/build.gradle index 514bcae735..7268b918a2 100644 --- a/FtcRobotController/build.gradle +++ b/FtcRobotController/build.gradle @@ -20,6 +20,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } + namespace = 'com.qualcomm.ftcrobotcontroller' } apply from: '../build.dependencies.gradle' diff --git a/FtcRobotController/src/main/AndroidManifest.xml b/FtcRobotController/src/main/AndroidManifest.xml index 5da443eb00..7ec8ab31d4 100644 --- a/FtcRobotController/src/main/AndroidManifest.xml +++ b/FtcRobotController/src/main/AndroidManifest.xml @@ -1,16 +1,14 @@ + android:versionCode="47" + android:versionName="8.0"> REVERSE ) of any wheel that runs backwards + // The motor reversals shown here are for a "direct drive" robot (the wheels turn the same direction as the motor shaft) + // If your robot has additional gear reductions or uses a right-angled drive, it's important to ensure + // that your motors are turning in the correct direction. So, start out with the reversals here, BUT + // when you first test your robot, push the left joystick forward and observe the direction the wheels turn. + // Reverse the direction (flip FORWARD <-> REVERSE ) of any wheel that runs backward + // Keep testing until ALL the wheels move the robot forward when you push the left joystick forward. leftFrontDrive.setDirection(DcMotor.Direction.REVERSE); leftBackDrive.setDirection(DcMotor.Direction.REVERSE); rightFrontDrive.setDirection(DcMotor.Direction.FORWARD); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Iterative.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Iterative.java index 9b96cfe2f6..e2cf9658c5 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Iterative.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Iterative.java @@ -40,13 +40,13 @@ * This file contains an example of an iterative (Non-Linear) "OpMode". * An OpMode is a 'program' that runs in either the autonomous or the teleop period of an FTC match. * The names of OpModes appear on the menu of the FTC Driver Station. - * When an selection is made from the menu, the corresponding OpMode + * When a selection is made from the menu, the corresponding OpMode * class is instantiated on the Robot Controller and executed. * * This particular OpMode just executes a basic Tank Drive Teleop for a two wheeled robot * It includes all the skeletal structure that all iterative OpModes contain. * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list */ @@ -72,10 +72,11 @@ public void init() { leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); - // Most robots need the motor on one side to be reversed to drive forward - // Reverse the motor that runs backwards when connected directly to the battery - leftDrive.setDirection(DcMotor.Direction.FORWARD); - rightDrive.setDirection(DcMotor.Direction.REVERSE); + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // Pushing the left stick forward MUST make robot go forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); // Tell the driver that initialization is complete. telemetry.addData("Status", "Initialized"); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Linear.java index 63b37bec5e..277be2c391 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Linear.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Linear.java @@ -40,13 +40,13 @@ /** * This file contains an minimal example of a Linear "OpMode". An OpMode is a 'program' that runs in either * the autonomous or the teleop period of an FTC match. The names of OpModes appear on the menu - * of the FTC Driver Station. When an selection is made from the menu, the corresponding OpMode + * of the FTC Driver Station. When a selection is made from the menu, the corresponding OpMode * class is instantiated on the Robot Controller and executed. * * This particular OpMode just executes a basic Tank Drive Teleop for a two wheeled robot * It includes all the skeletal structure that all linear OpModes contain. * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list */ @@ -70,10 +70,11 @@ public void runOpMode() { leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); - // Most robots need the motor on one side to be reversed to drive forward - // Reverse the motor that runs backwards when connected directly to the battery - leftDrive.setDirection(DcMotor.Direction.FORWARD); - rightDrive.setDirection(DcMotor.Direction.REVERSE); + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // Pushing the left stick forward MUST make robot go forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); // Wait for the game to start (driver presses PLAY) waitForStart(); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptCompassCalibration.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptCompassCalibration.java index cd1da0a85d..45dac81abe 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptCompassCalibration.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptCompassCalibration.java @@ -33,13 +33,11 @@ import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; import com.qualcomm.robotcore.eventloop.opmode.TeleOp; import com.qualcomm.robotcore.hardware.CompassSensor; +import com.qualcomm.robotcore.hardware.DcMotor; import com.qualcomm.robotcore.util.ElapsedTime; /** * This file illustrates the concept of calibrating a MR Compass - * It uses the common Pushbot hardware class to define the drive on the robot. - * The code is structured as a LinearOpMode - * * This code assumes there is a compass configured with the name "compass" * * This code will put the compass into calibration mode, wait three seconds and then attempt @@ -57,7 +55,8 @@ public class ConceptCompassCalibration extends LinearOpMode { /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // Use a Pushbot's hardware + public DcMotor leftDrive = null; + public DcMotor rightDrive = null; private ElapsedTime runtime = new ElapsedTime(); CompassSensor compass; @@ -68,10 +67,15 @@ public class ConceptCompassCalibration extends LinearOpMode { @Override public void runOpMode() { - /* Initialize the drive system variables. - * The init() method of the hardware class does all the work here - */ - robot.init(hardwareMap); + // Initialize the drive system variables. + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // Pushing the left stick forward MUST make robot go forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); // get a reference to our Compass Sensor object. compass = hardwareMap.get(CompassSensor.class, "compass"); @@ -93,8 +97,8 @@ public void runOpMode() { // Start the robot rotating clockwise telemetry.addData("Compass", "Calibration mode. Turning the robot..."); telemetry.update(); - robot.leftDrive.setPower(MOTOR_POWER); - robot.rightDrive.setPower(-MOTOR_POWER); + leftDrive.setPower(MOTOR_POWER); + rightDrive.setPower(-MOTOR_POWER); // run until time expires OR the driver presses STOP; runtime.reset(); @@ -103,8 +107,8 @@ public void runOpMode() { } // Stop all motors and turn off claibration - robot.leftDrive.setPower(0); - robot.rightDrive.setPower(0); + leftDrive.setPower(0); + rightDrive.setPower(0); compass.setMode(CompassSensor.CompassMode.MEASUREMENT_MODE); telemetry.addData("Compass", "Returning to measurement mode"); telemetry.update(); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptDIMAsIndicator.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptDIMAsIndicator.java deleted file mode 100644 index 75b6ad4458..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptDIMAsIndicator.java +++ /dev/null @@ -1,103 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -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.hardware.DeviceInterfaceModule; -import com.qualcomm.robotcore.util.ElapsedTime; - -/** - * This OpMode illustrates using the Device Interface Module as a signalling device. - * The code is structured as a LinearOpMode - * - * This code assumes a DIM name "dim". - * - * There are many examples where the robot might like to signal the driver, without requiring them - * to look at the driver station. This might be something like a "ball in hopper" condition or a - * "ready to shoot" condition. - * - * The DIM has two user settable indicator LEDs (one red one blue). These can be controlled - * directly from your program. - * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list - */ -@TeleOp(name = "Concept: DIM As Indicator", group = "Concept") -@Disabled -public class ConceptDIMAsIndicator extends LinearOpMode { - - static final int BLUE_LED = 0; // Blue LED channel on DIM - static final int RED_LED = 1; // Red LED Channel on DIM - - // Create timer to toggle LEDs - private ElapsedTime runtime = new ElapsedTime(); - - // Define class members - DeviceInterfaceModule dim; - - @Override - public void runOpMode() { - - // Connect to motor (Assume standard left wheel) - // Change the text in quotes to match any motor name on your robot. - dim = hardwareMap.get(DeviceInterfaceModule.class, "dim"); - - // Toggle LEDs while Waiting for the start button - telemetry.addData(">", "Press Play to test LEDs." ); - telemetry.update(); - - while (!isStarted()) { - // Determine if we are on an odd or even second - boolean even = (((int)(runtime.time()) & 0x01) == 0); - dim.setLED(RED_LED, even); // Red for even - dim.setLED(BLUE_LED, !even); // Blue for odd - idle(); - } - - // Running now - telemetry.addData(">", "Press X for Blue, B for Red." ); - telemetry.update(); - - // Now just use red and blue buttons to set red and blue LEDs - while(opModeIsActive()){ - dim.setLED(BLUE_LED, gamepad1.x); - dim.setLED(RED_LED, gamepad1.b); - idle(); - } - - // Turn off LEDs; - dim.setLED(BLUE_LED, false); - dim.setLED(RED_LED, false); - telemetry.addData(">", "Done"); - telemetry.update(); - } -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExternalHardwareClass.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExternalHardwareClass.java new file mode 100644 index 0000000000..6607fb6e6e --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExternalHardwareClass.java @@ -0,0 +1,142 @@ +/* Copyright (c) 2022 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; + +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.Range; + +/** + * This OpMode Sample illustrates how to use an external "hardware" class to modularize all the robot's sensors and actuators. + * This approach is very efficient because the same hardware class can be used by all of your teleop and autonomous OpModes + * without requiring many copy & paste operations. Once you have defined and tested the hardware class with one OpMode, + * it is instantly available to other OpModes. + * + * The real benefit of this approach is that as you tweak your robot hardware, you only need to make changes in ONE place (the Hardware Class). + * So, to be effective you should put as much or your hardware setup and access code as possible in the hardware class. + * Essentially anything you do with hardware in BOTH Teleop and Auto should likely go in the hardware class. + * + * The Hardware Class is created in a separate file, and then an "instance" of this class is created in each OpMode. + * In order for the class to do typical OpMode things (like send telemetry data) it must be passed a reference to the + * OpMode object when it's created, so it can access all core OpMode functions. This is illustrated below. + * + * In this concept sample, the hardware class file is called RobotHardware.java and it must accompany this sample OpMode. + * So, if you copy ConceptExternalHardwareClass.java into TeamCode (using Android Studio or OnBotJava) then RobotHardware.java + * must also be copied to the same location (maintaining its name). + * + * For comparison purposes, this sample and its accompanying hardware class duplicates the functionality of the + * RobotTelopPOV_Linear opmode. It assumes three motors (left_drive, right_drive and arm) and two servos (left_hand and right_hand) + * + * View the RobotHardware.java class file for more details + * + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * + * In OnBot Java, add a new OpMode, drawing from this Sample; select TeleOp. + * Also add another new file named RobotHardware.java, drawing from the Sample with that name; select Not an OpMode. + */ + +@TeleOp(name="Concept: Robot Hardware Class", group="Robot") +@Disabled +public class ConceptExternalHardwareClass extends LinearOpMode { + + // Create a RobotHardware object to be used to access robot hardware. + // Prefix any hardware functions with "robot." to access this class. + RobotHardware robot = new RobotHardware(this); + + @Override + public void runOpMode() { + double drive = 0; + double turn = 0; + double arm = 0; + double handOffset = 0; + + // initialize all the hardware, using the hardware class. See how clean and simple this is? + robot.init(); + + // Send telemetry message to signify robot waiting; + // Wait for the game to start (driver presses PLAY) + waitForStart(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + + // Run wheels in POV mode (note: The joystick goes negative when pushed forward, so negate it) + // In this mode the Left stick moves the robot fwd and back, the Right stick turns left and right. + // This way it's also easy to just drive straight, or just turn. + drive = -gamepad1.left_stick_y; + turn = gamepad1.right_stick_x; + + // Combine drive and turn for blended motion. Use RobotHardware class + robot.driveRobot(drive, turn); + + // Use gamepad left & right Bumpers to open and close the claw + // Use the SERVO constants defined in RobotHardware class. + // Each time around the loop, the servos will move by a small amount. + // Limit the total offset to half of the full travel range + if (gamepad1.right_bumper) + handOffset += robot.HAND_SPEED; + else if (gamepad1.left_bumper) + handOffset -= robot.HAND_SPEED; + handOffset = Range.clip(handOffset, -0.5, 0.5); + + // Move both servos to new position. Use RobotHardware class + robot.setHandPositions(handOffset); + + // Use gamepad buttons to move arm up (Y) and down (A) + // Use the MOTOR constants defined in RobotHardware class. + if (gamepad1.y) + arm = robot.ARM_UP_POWER; + else if (gamepad1.a) + arm = robot.ARM_DOWN_POWER; + else + arm = 0; + + robot.setArmPower(arm); + + // Send telemetry messages to explain controls and show robot status + telemetry.addData("Drive", "Left Stick"); + telemetry.addData("Turn", "Right Stick"); + telemetry.addData("Arm Up/Down", "Y & A Buttons"); + telemetry.addData("Hand Open/Closed", "Left and Right Bumpers"); + telemetry.addData("-", "-------"); + + telemetry.addData("Drive Power", "%.2f", drive); + telemetry.addData("Turn Power", "%.2f", turn); + telemetry.addData("Arm Power", "%.2f", arm); + telemetry.addData("Hand Position", "Offset = %.2f", handOffset); + telemetry.update(); + + // Pace this loop so hands move at a reasonable speed. + sleep(50); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptI2cAddressChange.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptI2cAddressChange.java deleted file mode 100644 index 4deef91844..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptI2cAddressChange.java +++ /dev/null @@ -1,223 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -import com.qualcomm.hardware.modernrobotics.ModernRoboticsUsbDeviceInterfaceModule; -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.hardware.DeviceInterfaceModule; -import com.qualcomm.robotcore.hardware.I2cAddr; -import com.qualcomm.robotcore.util.RobotLog; -import com.qualcomm.robotcore.util.TypeConversion; - -import java.util.concurrent.locks.Lock; - -/** - * An example of a linear op mode that shows how to change the I2C address. - */ -@TeleOp(name = "Concept: I2c Address Change", group = "Concept") -@Disabled -public class ConceptI2cAddressChange extends LinearOpMode { - - public static final int ADDRESS_SET_NEW_I2C_ADDRESS = 0x70; - // trigger bytes used to change I2C address on ModernRobotics sensors. - public static final byte TRIGGER_BYTE_1 = 0x55; - public static final byte TRIGGER_BYTE_2 = (byte) 0xaa; - - // Expected bytes from the Modern Robotics IR Seeker V3 memory map - public static final byte IR_SEEKER_V3_FIRMWARE_REV = 0x12; - public static final byte IR_SEEKER_V3_SENSOR_ID = 0x49; - public static final I2cAddr IR_SEEKER_V3_ORIGINAL_ADDRESS = I2cAddr.create8bit(0x38); - - // Expected bytes from the Modern Robotics Color Sensor memory map - public static final byte COLOR_SENSOR_FIRMWARE_REV = 0x10; - public static final byte COLOR_SENSOR_SENSOR_ID = 0x43; - public static final byte COLOR_SENSOR_ORIGINAL_ADDRESS = 0x3C; - - public static final byte MANUFACTURER_CODE = 0x4d; - // Currently, this is set to expect the bytes from the IR Seeker. - // If you change these values so you're setting "FIRMWARE_REV" to - // COLOR_SENSOR_FIRMWARE_REV, and "SENSOR_ID" to "COLOR_SENSOR_SENSOR_ID", - // you'll be able to change the I2C address of the ModernRoboticsColorSensor. - // If the bytes you're expecting are different than what this op mode finds, - // a comparison will be printed out into the logfile. - public static final byte FIRMWARE_REV = IR_SEEKER_V3_FIRMWARE_REV; - public static final byte SENSOR_ID = IR_SEEKER_V3_SENSOR_ID; - - // These byte values are common with most Modern Robotics sensors. - public static final int READ_MODE = 0x80; - public static final int ADDRESS_MEMORY_START = 0x0; - public static final int TOTAL_MEMORY_LENGTH = 0x0c; - public static final int BUFFER_CHANGE_ADDRESS_LENGTH = 0x03; - - // The port where your sensor is connected. - int port = 5; - - byte[] readCache; - Lock readLock; - byte[] writeCache; - Lock writeLock; - - I2cAddr currentAddress = IR_SEEKER_V3_ORIGINAL_ADDRESS; - // I2c addresses on Modern Robotics devices must be divisible by 2, and between 0x7e and 0x10 - // Different hardware may have different rules. - // Be sure to read the requirements for the hardware you're using! - // If you use an invalid address, you may make your device completely unusable. - I2cAddr newAddress = I2cAddr.create8bit(0x42); - - DeviceInterfaceModule dim; - - @Override - public void runOpMode() { - - // set up the hardware devices we are going to use - dim = hardwareMap.get(DeviceInterfaceModule.class, "dim"); - - readCache = dim.getI2cReadCache(port); - readLock = dim.getI2cReadCacheLock(port); - writeCache = dim.getI2cWriteCache(port); - writeLock = dim.getI2cWriteCacheLock(port); - - // I2c addresses on Modern Robotics devices must be divisible by 2, and between 0x7e and 0x10 - // Different hardware may have different rules. - // Be sure to read the requirements for the hardware you're using! - ModernRoboticsUsbDeviceInterfaceModule.throwIfModernRoboticsI2cAddressIsInvalid(newAddress); - - // wait for the start button to be pressed - waitForStart(); - - performAction("read", port, currentAddress, ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH); - - while(!dim.isI2cPortReady(port)) { - telemetry.addData("I2cAddressChange", "waiting for the port to be ready..."); - telemetry.update(); - sleep(1000); - } - - // update the local cache - dim.readI2cCacheFromController(port); - - // make sure the first bytes are what we think they should be. - int count = 0; - int[] initialArray = {READ_MODE, currentAddress.get8Bit(), ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH, FIRMWARE_REV, MANUFACTURER_CODE, SENSOR_ID}; - while (!foundExpectedBytes(initialArray, readLock, readCache)) { - telemetry.addData("I2cAddressChange", "Confirming that we're reading the correct bytes..."); - telemetry.update(); - dim.readI2cCacheFromController(port); - sleep(1000); - count++; - // if we go too long with failure, we probably are expecting the wrong bytes. - if (count >= 10) { - telemetry.addData("I2cAddressChange", String.format("Looping too long with no change, probably have the wrong address. Current address: 8bit=0x%02x", currentAddress.get8Bit())); - hardwareMap.irSeekerSensor.get(String.format("Looping too long with no change, probably have the wrong address. Current address: 8bit=0x%02x", currentAddress.get8Bit())); - telemetry.update(); - } - } - - // Enable writes to the correct segment of the memory map. - performAction("write", port, currentAddress, ADDRESS_SET_NEW_I2C_ADDRESS, BUFFER_CHANGE_ADDRESS_LENGTH); - - // Write out the trigger bytes, and the new desired address. - writeNewAddress(); - dim.setI2cPortActionFlag(port); - dim.writeI2cCacheToController(port); - - telemetry.addData("I2cAddressChange", "Giving the hardware 60 seconds to make the change..."); - telemetry.update(); - - // Changing the I2C address takes some time. - sleep(60000); - - // Query the new address and see if we can get the bytes we expect. - dim.enableI2cReadMode(port, newAddress, ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH); - dim.setI2cPortActionFlag(port); - dim.writeI2cCacheToController(port); - - int[] confirmArray = {READ_MODE, newAddress.get8Bit(), ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH, FIRMWARE_REV, MANUFACTURER_CODE, SENSOR_ID}; - while (!foundExpectedBytes(confirmArray, readLock, readCache)) { - telemetry.addData("I2cAddressChange", "Have not confirmed the changes yet..."); - telemetry.update(); - dim.readI2cCacheFromController(port); - sleep(1000); - } - - telemetry.addData("I2cAddressChange", "Successfully changed the I2C address. New address: 8bit=0x%02x", newAddress.get8Bit()); - telemetry.update(); - RobotLog.i("Successfully changed the I2C address." + String.format("New address: 8bit=0x%02x", newAddress.get8Bit())); - - /**** IMPORTANT NOTE ******/ - // You need to add a line like this at the top of your op mode - // to update the I2cAddress in the driver. - //irSeeker.setI2cAddress(newAddress); - /***************************/ - - } - - private boolean foundExpectedBytes(int[] byteArray, Lock lock, byte[] cache) { - try { - lock.lock(); - boolean allMatch = true; - StringBuilder s = new StringBuilder(300 * 4); - String mismatch = ""; - for (int i = 0; i < byteArray.length; i++) { - s.append(String.format("expected: %02x, got: %02x \n", TypeConversion.unsignedByteToInt( (byte) byteArray[i]), cache[i])); - if (TypeConversion.unsignedByteToInt(cache[i]) != TypeConversion.unsignedByteToInt( (byte) byteArray[i])) { - mismatch = String.format("i: %d, byteArray[i]: %02x, cache[i]: %02x", i, byteArray[i], cache[i]); - allMatch = false; - } - } - RobotLog.e(s.toString() + "\n allMatch: " + allMatch + ", mismatch: " + mismatch); - return allMatch; - } finally { - lock.unlock(); - } - } - - private void performAction(String actionName, int port, I2cAddr i2cAddress, int memAddress, int memLength) { - if (actionName.equalsIgnoreCase("read")) dim.enableI2cReadMode(port, i2cAddress, memAddress, memLength); - if (actionName.equalsIgnoreCase("write")) dim.enableI2cWriteMode(port, i2cAddress, memAddress, memLength); - - dim.setI2cPortActionFlag(port); - dim.writeI2cCacheToController(port); - dim.readI2cCacheFromController(port); - } - - private void writeNewAddress() { - try { - writeLock.lock(); - writeCache[4] = (byte) newAddress.get8Bit(); - writeCache[5] = TRIGGER_BYTE_1; - writeCache[6] = TRIGGER_BYTE_2; - } finally { - writeLock.unlock(); - } - } -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptMotorBulkRead.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptMotorBulkRead.java index 4e6f52f8d9..e0c5a75de4 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptMotorBulkRead.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptMotorBulkRead.java @@ -46,21 +46,28 @@ Three scenarios are tested: Cache Mode = OFF This is the normal default, where no cache is used, and every read produces a discrete transaction with - an expansion hub, which is the slowest approach. + an expansion hub, which is the slowest approach, but guarentees that the value is as fresh (recent) as possible.. + Cache Mode = AUTO This mode will attempt to minimize the number of discrete read commands, by performing bulk-reads - and then returning values that have been cached. The cache is updated automatically whenever a specific read operation is repeated. - This mode will always return fresh data, but it may perform more bulk-reads than absolutely required. - Extra reads will be performed if multiple identical encoder/velocity reads are performed in one control cycle. + and then returning values that have been cached. The cache is updated automatically whenever any specific encoder is re-read. + This mode will always return new data, but it may perform more bulk-reads than absolutely required. + Extra reads will be performed if multiple encoder/velocity reads are performed on the same encoder in one control cycle. This mode is a good compromise between the OFF and MANUAL modes. - Cache Mode = MANUAL This mode enables the user's code to determine the best time to refresh the cached bulk-read data. - Well organized code can place all the sensor reads in one location, and then just reset the cache once per control cycle. - The approach will produce the shortest cycle times, but it does require the user to manually clear the cache. + Note: If there are significant user-program delays between encoder reads, the cached value may not be fresh (recent). + You can issue a clearBulkCache() call at any time force a fresh bulk-read on the next encoder read. + + Cache Mode = MANUAL This mode requires the user's code to determine the best time to clear the cached bulk-read data. + Well organized code will reset the cache once at the beginning of the control cycle, and then immediately read and store all the encoder values. + This approach will produce the shortest cycle times, but it does require the user to manually clear the cache. + Since NO automatic Bulk-Reads are performed, neglecting to clear the bulk cache will result in the same values being returned + each time an encoder read is performed. ------------------------------------- General tip to speed up your control cycles: + No matter what method you use to read encoders and other inputs, you should try to - avoid reading the same input multiple times around a control loop. + avoid reading the same encoder input multiple times around a control loop. Under normal conditions, this will slow down the control loop. The preferred method is to read all the required inputs ONCE at the beginning of the loop, and save the values in variable that can be used by other parts of the control code. diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRampMotorSpeed.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRampMotorSpeed.java index 0a8f3dcc28..f891142c62 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRampMotorSpeed.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRampMotorSpeed.java @@ -38,7 +38,7 @@ * This OpMode ramps a single motor speed up and down repeatedly until Stop is pressed. * The code is structured as a LinearOpMode * - * This code assumes a DC motor configured with the name "left_drive" as is found on a pushbot. + * This code assumes a DC motor configured with the name "left_drive" as is found on a Robot. * * INCREMENT sets how much to increase/decrease the power each cycle * CYCLE_MS sets the update period. diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevSPARKMini.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevSPARKMini.java index 1b1ecc30b1..6f2aa70647 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevSPARKMini.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevSPARKMini.java @@ -44,8 +44,8 @@ * robot configuration, use the drop down list under 'Servos' to select 'REV SPARKmini Controller' * and name them 'left_drive' and 'right_drive'. * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list */ @TeleOp(name="REV SPARKmini Simple Drive Example", group="Concept") @@ -69,7 +69,7 @@ public void runOpMode() { rightDrive = hardwareMap.get(DcMotorSimple.class, "right_drive"); // Most robots need the motor on one side to be reversed to drive forward - // Reverse the motor that runs backwards when connected directly to the battery + // Reverse the motor that runs backward when connected directly to the battery leftDrive.setDirection(DcMotorSimple.Direction.FORWARD); rightDrive.setDirection(DcMotorSimple.Direction.REVERSE); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptScanServo.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptScanServo.java index 8e9c899e40..38c7eb0961 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptScanServo.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptScanServo.java @@ -35,12 +35,12 @@ import com.qualcomm.robotcore.hardware.Servo; /** - * This OpMode scans a single servo back and forwards until Stop is pressed. + * This OpMode scans a single servo back and forward until Stop is pressed. * The code is structured as a LinearOpMode * INCREMENT sets how much to increase/decrease the servo position each cycle * CYCLE_MS sets the update period. * - * This code assumes a Servo configured with the name "left_hand" as is found on a pushbot. + * This code assumes a Servo configured with the name "left_hand" as is found on a Robot. * * NOTE: When any servo position is set, ALL attached servos are activated, so ensure that any other * connected servos are able to move freely before running this test. @@ -66,7 +66,7 @@ public class ConceptScanServo extends LinearOpMode { @Override public void runOpMode() { - // Connect to servo (Assume PushBot Left Hand) + // Connect to servo (Assume Robot Left Hand) // Change the text in quotes to match any servo name on your robot. servo = hardwareMap.get(Servo.class, "left_hand"); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsASJava.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsASJava.java index 51d99d208c..5c961adc4a 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsASJava.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsASJava.java @@ -43,7 +43,7 @@ * * If you are using OnBotJava, please see the ConceptSoundsOnBotJava sample * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list * * Operation: diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsSKYSTONE.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsSKYSTONE.java index e8f3dde91d..88329579ac 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsSKYSTONE.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsSKYSTONE.java @@ -41,8 +41,8 @@ * It does this by creating a simple "chooser" controlled by the gamepad Up Down buttons. * This code also prevents sounds from stacking up by setting a "playing" flag, which is cleared when the sound finishes playing. * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list * * Operation: * Use the DPAD to change the selected sound, and the Right Bumper to play it. diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetection.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetection.java index 06436eb30d..cb52ebdd96 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetection.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetection.java @@ -40,11 +40,11 @@ import org.firstinspires.ftc.robotcore.external.tfod.Recognition; /** - * This 2020-2021 OpMode illustrates the basics of using the TensorFlow Object Detection API to - * determine the position of the Freight Frenzy game elements. + * This 2022-2023 OpMode illustrates the basics of using the TensorFlow Object Detection API to + * determine which image is being presented to the robot. * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. * * IMPORTANT: In order to use this OpMode, you need to obtain your own Vuforia license key as * is explained below. @@ -52,23 +52,21 @@ @TeleOp(name = "Concept: TensorFlow Object Detection", group = "Concept") @Disabled public class ConceptTensorFlowObjectDetection extends LinearOpMode { - /* Note: This sample uses the all-objects Tensor Flow model (FreightFrenzy_BCDM.tflite), which contains - * the following 4 detectable objects - * 0: Ball, - * 1: Cube, - * 2: Duck, - * 3: Marker (duck location tape marker) - * - * Two additional model assets are available which only contain a subset of the objects: - * FreightFrenzy_BC.tflite 0: Ball, 1: Cube - * FreightFrenzy_DM.tflite 0: Duck, 1: Marker - */ - private static final String TFOD_MODEL_ASSET = "FreightFrenzy_BCDM.tflite"; + + /* + * Specify the source for the Tensor Flow Model. + * If the TensorFlowLite object model is included in the Robot Controller App as an "asset", + * the OpMode must to load it using loadModelFromAsset(). However, if a team generated model + * has been downloaded to the Robot Controller's SD FLASH memory, it must to be loaded using loadModelFromFile() + * Here we assume it's an Asset. Also see method initTfod() below . + */ + private static final String TFOD_MODEL_ASSET = "PowerPlay.tflite"; + // private static final String TFOD_MODEL_FILE = "/sdcard/FIRST/tflitemodels/CustomTeamModel.tflite"; + private static final String[] LABELS = { - "Ball", - "Cube", - "Duck", - "Marker" + "1 Bolt", + "2 Bulb", + "3 Panel" }; /* @@ -114,11 +112,11 @@ public void runOpMode() { // The TensorFlow software will scale the input images from the camera to a lower resolution. // This can result in lower detection accuracy at longer distances (> 55cm or 22"). - // If your target is at distance greater than 50 cm (20") you can adjust the magnification value + // If your target is at distance greater than 50 cm (20") you can increase the magnification value // to artificially zoom in to the center of image. For best results, the "aspectRatio" argument // should be set to the value of the images used to create the TensorFlow Object Detection model // (typically 16/9). - tfod.setZoom(2.5, 16.0/9.0); + tfod.setZoom(1.0, 16.0/9.0); } /** Wait for the game to begin */ @@ -133,19 +131,22 @@ public void runOpMode() { // the last time that call was made. List updatedRecognitions = tfod.getUpdatedRecognitions(); if (updatedRecognitions != null) { - telemetry.addData("# Object Detected", updatedRecognitions.size()); - - // step through the list of recognitions and display boundary info. - int i = 0; - for (Recognition recognition : updatedRecognitions) { - telemetry.addData(String.format("label (%d)", i), recognition.getLabel()); - telemetry.addData(String.format(" left,top (%d)", i), "%.03f , %.03f", - recognition.getLeft(), recognition.getTop()); - telemetry.addData(String.format(" right,bottom (%d)", i), "%.03f , %.03f", - recognition.getRight(), recognition.getBottom()); - i++; - } - telemetry.update(); + telemetry.addData("# Objects Detected", updatedRecognitions.size()); + + // step through the list of recognitions and display image position/size information for each one + // Note: "Image number" refers to the randomized image orientation/number + for (Recognition recognition : updatedRecognitions) { + double col = (recognition.getLeft() + recognition.getRight()) / 2 ; + double row = (recognition.getTop() + recognition.getBottom()) / 2 ; + double width = Math.abs(recognition.getRight() - recognition.getLeft()) ; + double height = Math.abs(recognition.getTop() - recognition.getBottom()) ; + + telemetry.addData(""," "); + telemetry.addData("Image", "%s (%.0f %% Conf.)", recognition.getLabel(), recognition.getConfidence() * 100 ); + telemetry.addData("- Position (Row/Col)","%.0f / %.0f", row, col); + telemetry.addData("- Size (Width/Height)","%.0f / %.0f", width, height); + } + telemetry.update(); } } } @@ -166,8 +167,6 @@ private void initVuforia() { // Instantiate the Vuforia engine vuforia = ClassFactory.getInstance().createVuforia(parameters); - - // Loading trackables is not necessary for the TensorFlow Object Detection engine. } /** @@ -177,10 +176,14 @@ private void initTfod() { int tfodMonitorViewId = hardwareMap.appContext.getResources().getIdentifier( "tfodMonitorViewId", "id", hardwareMap.appContext.getPackageName()); TFObjectDetector.Parameters tfodParameters = new TFObjectDetector.Parameters(tfodMonitorViewId); - tfodParameters.minResultConfidence = 0.8f; + tfodParameters.minResultConfidence = 0.75f; tfodParameters.isModelTensorFlow2 = true; - tfodParameters.inputSize = 320; + tfodParameters.inputSize = 300; tfod = ClassFactory.getInstance().createTFObjectDetector(tfodParameters, vuforia); + + // Use loadModelFromAsset() if the TF Model is built in as an asset by Android Studio + // Use loadModelFromFile() if you have downloaded a custom team model to the Robot Controller's FLASH. tfod.loadModelFromAsset(TFOD_MODEL_ASSET, LABELS); + // tfod.loadModelFromFile(TFOD_MODEL_FILE, LABELS); } } diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionSwitchableCameras.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionSwitchableCameras.java index f8c7df169f..eec021fd86 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionSwitchableCameras.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionSwitchableCameras.java @@ -41,8 +41,8 @@ import org.firstinspires.ftc.robotcore.external.tfod.Recognition; /** - * This 2020-2021 OpMode illustrates the basics of using the TensorFlow Object Detection API to - * determine the position of the Freight Frenzy game elements. + * This 2022-2023 OpMode illustrates the basics of using the TensorFlow Object Detection API to + * determine which image is being presented to the robot. * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. @@ -53,23 +53,21 @@ @TeleOp(name = "Concept: TensorFlow Object Detection Switchable Cameras", group = "Concept") @Disabled public class ConceptTensorFlowObjectDetectionSwitchableCameras extends LinearOpMode { - /* Note: This sample uses the all-objects Tensor Flow model (FreightFrenzy_BCDM.tflite), which contains - * the following 4 detectable objects - * 0: Ball, - * 1: Cube, - * 2: Duck, - * 3: Marker (duck location tape marker) - * - * Two additional model assets are available which only contain a subset of the objects: - * FreightFrenzy_BC.tflite 0: Ball, 1: Cube - * FreightFrenzy_DM.tflite 0: Duck, 1: Marker - */ - private static final String TFOD_MODEL_ASSET = "FreightFrenzy_BCDM.tflite"; + + /* + * Specify the source for the Tensor Flow Model. + * If the TensorFlowLite object model is included in the Robot Controller App as an "asset", + * the OpMode must to load it using loadModelFromAsset(). However, if a team generated model + * has been downloaded to the Robot Controller's SD FLASH memory, it must to be loaded using loadModelFromFile() + * Here we assume it's an Asset. Also see method initTfod() below . + */ + private static final String TFOD_MODEL_ASSET = "PowerPlay.tflite"; + // private static final String TFOD_MODEL_FILE = "/sdcard/FIRST/tflitemodels/CustomTeamModel.tflite"; + private static final String[] LABELS = { - "Ball", - "Cube", - "Duck", - "Marker" + "1 Bolt", + "2 Bulb", + "3 Panel" }; /* @@ -123,11 +121,11 @@ public void runOpMode() { // The TensorFlow software will scale the input images from the camera to a lower resolution. // This can result in lower detection accuracy at longer distances (> 55cm or 22"). - // If your target is at distance greater than 50 cm (20") you can adjust the magnification value + // If your target is at distance greater than 50 cm (20") you can increase the magnification value // to artificially zoom in to the center of image. For best results, the "aspectRatio" argument // should be set to the value of the images used to create the TensorFlow Object Detection model // (typically 16/9). - tfod.setZoom(2.5, 16.0/9.0); + tfod.setZoom(1.0, 16.0/9.0); } /** Wait for the game to begin */ @@ -140,16 +138,19 @@ public void runOpMode() { if (tfod != null) { doCameraSwitching(); List recognitions = tfod.getRecognitions(); - telemetry.addData("# Object Detected", recognitions.size()); - // step through the list of recognitions and display boundary info. - int i = 0; + telemetry.addData("# Objects Detected", recognitions.size()); + // step through the list of recognitions and display image size and position + // Note: "Image number" refers to the randomized image orientation/number for (Recognition recognition : recognitions) { - telemetry.addData(String.format("label (%d)", i), recognition.getLabel()); - telemetry.addData(String.format(" left,top (%d)", i), "%.03f , %.03f", - recognition.getLeft(), recognition.getTop()); - telemetry.addData(String.format(" right,bottom (%d)", i), "%.03f , %.03f", - recognition.getRight(), recognition.getBottom()); - i++; + double col = (recognition.getLeft() + recognition.getRight()) / 2 ; + double row = (recognition.getTop() + recognition.getBottom()) / 2 ; + double width = Math.abs(recognition.getRight() - recognition.getLeft()) ; + double height = Math.abs(recognition.getTop() - recognition.getBottom()) ; + + telemetry.addData(""," "); + telemetry.addData("Image", "%s (%.0f %% Conf.)", recognition.getLabel(), recognition.getConfidence() * 100 ); + telemetry.addData("- Position (Row/Col)","%.0f / %.0f", row, col); + telemetry.addData("- Size (Width/Height)","%.0f / %.0f", width, height); } telemetry.update(); } @@ -179,8 +180,6 @@ private void initVuforia() { // Set the active camera to Webcam 1. switchableCamera = (SwitchableCamera) vuforia.getCamera(); switchableCamera.setActiveCamera(webcam1); - - // Loading trackables is not necessary for the TensorFlow Object Detection engine. } /** @@ -190,11 +189,15 @@ private void initTfod() { int tfodMonitorViewId = hardwareMap.appContext.getResources().getIdentifier( "tfodMonitorViewId", "id", hardwareMap.appContext.getPackageName()); TFObjectDetector.Parameters tfodParameters = new TFObjectDetector.Parameters(tfodMonitorViewId); - tfodParameters.minResultConfidence = 0.8f; + tfodParameters.minResultConfidence = 0.75f; tfodParameters.isModelTensorFlow2 = true; - tfodParameters.inputSize = 320; + tfodParameters.inputSize = 300; tfod = ClassFactory.getInstance().createTFObjectDetector(tfodParameters, vuforia); + + // Use loadModelFromAsset() if the TF Model is built in as an asset by Android Studio + // Use loadModelFromFile() if you have downloaded a custom team model to the Robot Controller's FLASH. tfod.loadModelFromAsset(TFOD_MODEL_ASSET, LABELS); + // tfod.loadModelFromFile(TFOD_MODEL_FILE, LABELS); } private void doCameraSwitching() { diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionWebcam.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionWebcam.java index 08208637be..78bfef90f0 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionWebcam.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTensorFlowObjectDetectionWebcam.java @@ -40,8 +40,8 @@ import org.firstinspires.ftc.robotcore.external.tfod.Recognition; /** - * This 2020-2021 OpMode illustrates the basics of using the TensorFlow Object Detection API to - * determine the position of the Freight Frenzy game elements. + * This 2022-2023 OpMode illustrates the basics of using the TensorFlow Object Detection API to + * determine which image is being presented to the robot. * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. @@ -52,23 +52,22 @@ @TeleOp(name = "Concept: TensorFlow Object Detection Webcam", group = "Concept") @Disabled public class ConceptTensorFlowObjectDetectionWebcam extends LinearOpMode { - /* Note: This sample uses the all-objects Tensor Flow model (FreightFrenzy_BCDM.tflite), which contains - * the following 4 detectable objects - * 0: Ball, - * 1: Cube, - * 2: Duck, - * 3: Marker (duck location tape marker) - * - * Two additional model assets are available which only contain a subset of the objects: - * FreightFrenzy_BC.tflite 0: Ball, 1: Cube - * FreightFrenzy_DM.tflite 0: Duck, 1: Marker - */ - private static final String TFOD_MODEL_ASSET = "FreightFrenzy_BCDM.tflite"; + + /* + * Specify the source for the Tensor Flow Model. + * If the TensorFlowLite object model is included in the Robot Controller App as an "asset", + * the OpMode must to load it using loadModelFromAsset(). However, if a team generated model + * has been downloaded to the Robot Controller's SD FLASH memory, it must to be loaded using loadModelFromFile() + * Here we assume it's an Asset. Also see method initTfod() below . + */ + private static final String TFOD_MODEL_ASSET = "PowerPlay.tflite"; + // private static final String TFOD_MODEL_FILE = "/sdcard/FIRST/tflitemodels/CustomTeamModel.tflite"; + + private static final String[] LABELS = { - "Ball", - "Cube", - "Duck", - "Marker" + "1 Bolt", + "2 Bulb", + "3 Panel" }; /* @@ -114,11 +113,11 @@ public void runOpMode() { // The TensorFlow software will scale the input images from the camera to a lower resolution. // This can result in lower detection accuracy at longer distances (> 55cm or 22"). - // If your target is at distance greater than 50 cm (20") you can adjust the magnification value + // If your target is at distance greater than 50 cm (20") you can increase the magnification value // to artificially zoom in to the center of image. For best results, the "aspectRatio" argument // should be set to the value of the images used to create the TensorFlow Object Detection model // (typically 16/9). - tfod.setZoom(2.5, 16.0/9.0); + tfod.setZoom(1.0, 16.0/9.0); } /** Wait for the game to begin */ @@ -133,18 +132,22 @@ public void runOpMode() { // the last time that call was made. List updatedRecognitions = tfod.getUpdatedRecognitions(); if (updatedRecognitions != null) { - telemetry.addData("# Object Detected", updatedRecognitions.size()); - // step through the list of recognitions and display boundary info. - int i = 0; - for (Recognition recognition : updatedRecognitions) { - telemetry.addData(String.format("label (%d)", i), recognition.getLabel()); - telemetry.addData(String.format(" left,top (%d)", i), "%.03f , %.03f", - recognition.getLeft(), recognition.getTop()); - telemetry.addData(String.format(" right,bottom (%d)", i), "%.03f , %.03f", - recognition.getRight(), recognition.getBottom()); - i++; - } - telemetry.update(); + telemetry.addData("# Objects Detected", updatedRecognitions.size()); + + // step through the list of recognitions and display image position/size information for each one + // Note: "Image number" refers to the randomized image orientation/number + for (Recognition recognition : updatedRecognitions) { + double col = (recognition.getLeft() + recognition.getRight()) / 2 ; + double row = (recognition.getTop() + recognition.getBottom()) / 2 ; + double width = Math.abs(recognition.getRight() - recognition.getLeft()) ; + double height = Math.abs(recognition.getTop() - recognition.getBottom()) ; + + telemetry.addData(""," "); + telemetry.addData("Image", "%s (%.0f %% Conf.)", recognition.getLabel(), recognition.getConfidence() * 100 ); + telemetry.addData("- Position (Row/Col)","%.0f / %.0f", row, col); + telemetry.addData("- Size (Width/Height)","%.0f / %.0f", width, height); + } + telemetry.update(); } } } @@ -165,8 +168,6 @@ private void initVuforia() { // Instantiate the Vuforia engine vuforia = ClassFactory.getInstance().createVuforia(parameters); - - // Loading trackables is not necessary for the TensorFlow Object Detection engine. } /** @@ -176,10 +177,14 @@ private void initTfod() { int tfodMonitorViewId = hardwareMap.appContext.getResources().getIdentifier( "tfodMonitorViewId", "id", hardwareMap.appContext.getPackageName()); TFObjectDetector.Parameters tfodParameters = new TFObjectDetector.Parameters(tfodMonitorViewId); - tfodParameters.minResultConfidence = 0.8f; - tfodParameters.isModelTensorFlow2 = true; - tfodParameters.inputSize = 320; - tfod = ClassFactory.getInstance().createTFObjectDetector(tfodParameters, vuforia); - tfod.loadModelFromAsset(TFOD_MODEL_ASSET, LABELS); + tfodParameters.minResultConfidence = 0.75f; + tfodParameters.isModelTensorFlow2 = true; + tfodParameters.inputSize = 300; + tfod = ClassFactory.getInstance().createTFObjectDetector(tfodParameters, vuforia); + + // Use loadModelFromAsset() if the TF Model is built in as an asset by Android Studio + // Use loadModelFromFile() if you have downloaded a custom team model to the Robot Controller's FLASH. + tfod.loadModelFromAsset(TFOD_MODEL_ASSET, LABELS); + // tfod.loadModelFromFile(TFOD_MODEL_FILE, LABELS); } } diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentification.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentification.java index f0d735bfb8..3bdb1af2c1 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentification.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentification.java @@ -49,20 +49,20 @@ /** * This OpMode illustrates the basics of using the Vuforia engine to determine * the identity of Vuforia VuMarks encountered on the field. The code is structured as - * a LinearOpMode. It shares much structure with {@link ConceptVuforiaNavigation}; we do not here + * a LinearOpMode. It shares much structure with {@link ConceptVuforiaFieldNavigation}; we do not here * duplicate the core Vuforia documentation found there, but rather instead focus on the * differences between the use of Vuforia for navigation vs VuMark identification. * - * @see ConceptVuforiaNavigation + * @see ConceptVuforiaFieldNavigation * @see VuforiaLocalizer * @see VuforiaTrackableDefaultListener * see ftc_app/doc/tutorial/FTC_FieldCoordinateSystemDefinition.pdf * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. * * IMPORTANT: In order to use this OpMode, you need to obtain your own Vuforia license key as - * is explained in {@link ConceptVuforiaNavigation}. + * is explained below. */ @TeleOp(name="Concept: VuMark Id", group ="Concept") diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentificationWebcam.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentificationWebcam.java index 866261f223..14d2aff5f2 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentificationWebcam.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuMarkIdentificationWebcam.java @@ -50,20 +50,20 @@ /** * This OpMode illustrates the basics of using the Vuforia engine to determine * the identity of Vuforia VuMarks encountered on the field. The code is structured as - * a LinearOpMode. It shares much structure with {@link ConceptVuforiaNavigationWebcam}; we do not here + * a LinearOpMode. It shares much structure with {@link ConceptVuforiaFieldNavigationWebcam}; we do not here * duplicate the core Vuforia documentation found there, but rather instead focus on the * differences between the use of Vuforia for navigation vs VuMark identification. * - * @see ConceptVuforiaNavigationWebcam + * @see ConceptVuforiaFieldNavigationWebcam * @see VuforiaLocalizer * @see VuforiaTrackableDefaultListener * see ftc_app/doc/tutorial/FTC_FieldCoordinateSystemDefinition.pdf * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. * * IMPORTANT: In order to use this OpMode, you need to obtain your own Vuforia license key as - * is explained in {@link ConceptVuforiaNavigationWebcam}. + * is explained below */ @TeleOp(name="Concept: VuMark Id Webcam", group ="Concept") diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaDriveToTargetWebcam.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaDriveToTargetWebcam.java index 0175964e69..d41ada1fa3 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaDriveToTargetWebcam.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaDriveToTargetWebcam.java @@ -96,14 +96,14 @@ public class ConceptVuforiaDriveToTargetWebcam extends LinearOpMode this.vuforia = ClassFactory.getInstance().createVuforia(parameters); // Load the trackable objects from the Assets file, and give them meaningful names - VuforiaTrackables targetsFreightFrenzy = this.vuforia.loadTrackablesFromAsset("FreightFrenzy"); - targetsFreightFrenzy.get(0).setName("Blue Storage"); - targetsFreightFrenzy.get(1).setName("Blue Alliance Wall"); - targetsFreightFrenzy.get(2).setName("Red Storage"); - targetsFreightFrenzy.get(3).setName("Red Alliance Wall"); + VuforiaTrackables targetsPowerPlay = this.vuforia.loadTrackablesFromAsset("PowerPlay"); + targetsPowerPlay.get(0).setName("Red Audience Wall"); + targetsPowerPlay.get(1).setName("Red Rear Wall"); + targetsPowerPlay.get(2).setName("Blue Audience Wall"); + targetsPowerPlay.get(3).setName("Blue Rear Wall"); // Start tracking targets in the background - targetsFreightFrenzy.activate(); + targetsPowerPlay.activate(); // Initialize the hardware variables. Note that the strings used here as parameters // to 'get' must correspond to the names assigned during the robot configuration @@ -112,9 +112,10 @@ public class ConceptVuforiaDriveToTargetWebcam extends LinearOpMode rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. - // Pushing the left stick forward MUST make robot go forward. So adjust these two lines based on your first test drive. - leftDrive.setDirection(DcMotor.Direction.FORWARD); - rightDrive.setDirection(DcMotor.Direction.REVERSE); + // When run, this OpMode should start both motors driving forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); telemetry.addData(">", "Press Play to start"); telemetry.update(); @@ -131,7 +132,7 @@ public class ConceptVuforiaDriveToTargetWebcam extends LinearOpMode { // Look for first visible target, and save its pose. targetFound = false; - for (VuforiaTrackable trackable : targetsFreightFrenzy) + for (VuforiaTrackable trackable : targetsPowerPlay) { if (((VuforiaTrackableDefaultListener) trackable.getListener()).isVisible()) { diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigation.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigation.java index d9d13dc6a0..84f2e7bc8c 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigation.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigation.java @@ -67,7 +67,7 @@ * To learn more about the FTC field coordinate model, see FTC_FieldCoordinateSystemDefinition.pdf in this folder * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. * * IMPORTANT: In order to use this OpMode, you need to obtain your own Vuforia license key as * is explained below. @@ -100,7 +100,7 @@ public class ConceptVuforiaFieldNavigation extends LinearOpMode { " -- YOUR NEW VUFORIA KEY GOES HERE --- "; // Since ImageTarget trackables use mm to specifiy their dimensions, we must use mm for all the physical dimension. - // We will define some constants and conversions here. These are useful for the Freight Frenzy field. + // We will define some constants and conversions here. These are useful for the FTC competition field. private static final float mmPerInch = 25.4f; private static final float mmTargetHeight = 6 * mmPerInch; // the height of the center of the target image above the floor private static final float halfField = 72 * mmPerInch; @@ -136,9 +136,8 @@ public class ConceptVuforiaFieldNavigation extends LinearOpMode { // Instantiate the Vuforia engine vuforia = ClassFactory.getInstance().createVuforia(parameters); - // Load the data sets for the trackable objects. These particular data - // sets are stored in the 'assets' part of our application. - targets = this.vuforia.loadTrackablesFromAsset("FreightFrenzy"); + // Load the trackable assets. + targets = this.vuforia.loadTrackablesFromAsset("PowerPlay"); // For convenience, gather together all the trackable objects in one easily-iterable collection */ List allTrackables = new ArrayList(); @@ -163,10 +162,10 @@ public class ConceptVuforiaFieldNavigation extends LinearOpMode { */ // Name and locate each trackable object - identifyTarget(0, "Blue Storage", -halfField, oneAndHalfTile, mmTargetHeight, 90, 0, 90); - identifyTarget(1, "Blue Alliance Wall", halfTile, halfField, mmTargetHeight, 90, 0, 0); - identifyTarget(2, "Red Storage", -halfField, -oneAndHalfTile, mmTargetHeight, 90, 0, 90); - identifyTarget(3, "Red Alliance Wall", halfTile, -halfField, mmTargetHeight, 90, 0, 180); + identifyTarget(0, "Red Audience Wall", -halfField, -oneAndHalfTile, mmTargetHeight, 90, 0, 90); + identifyTarget(1, "Red Rear Wall", halfField, -oneAndHalfTile, mmTargetHeight, 90, 0, -90); + identifyTarget(2, "Blue Audience Wall", -halfField, oneAndHalfTile, mmTargetHeight, 90, 0, 90); + identifyTarget(3, "Blue Rear Wall", halfField, oneAndHalfTile, mmTargetHeight, 90, 0, -90); /* * Create a transformation matrix describing where the phone is on the robot. diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigationWebcam.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigationWebcam.java index c19ee2279d..6316c48dd4 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigationWebcam.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVuforiaFieldNavigationWebcam.java @@ -69,7 +69,7 @@ * To learn more about the FTC field coordinate model, see FTC_FieldCoordinateSystemDefinition.pdf in this folder * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. * * IMPORTANT: In order to use this OpMode, you need to obtain your own Vuforia license key as * is explained below. @@ -137,7 +137,7 @@ public class ConceptVuforiaFieldNavigationWebcam extends LinearOpMode { // Load the data sets for the trackable objects. These particular data // sets are stored in the 'assets' part of our application. - targets = this.vuforia.loadTrackablesFromAsset("FreightFrenzy"); + targets = this.vuforia.loadTrackablesFromAsset("PowerPlay"); // For convenience, gather together all the trackable objects in one easily-iterable collection */ List allTrackables = new ArrayList(); @@ -162,10 +162,10 @@ public class ConceptVuforiaFieldNavigationWebcam extends LinearOpMode { */ // Name and locate each trackable object - identifyTarget(0, "Blue Storage", -halfField, oneAndHalfTile, mmTargetHeight, 90, 0, 90); - identifyTarget(1, "Blue Alliance Wall", halfTile, halfField, mmTargetHeight, 90, 0, 0); - identifyTarget(2, "Red Storage", -halfField, -oneAndHalfTile, mmTargetHeight, 90, 0, 90); - identifyTarget(3, "Red Alliance Wall", halfTile, -halfField, mmTargetHeight, 90, 0, 180); + identifyTarget(0, "Red Audience Wall", -halfField, -oneAndHalfTile, mmTargetHeight, 90, 0, 90); + identifyTarget(1, "Red Rear Wall", halfField, -oneAndHalfTile, mmTargetHeight, 90, 0, -90); + identifyTarget(2, "Blue Audience Wall", -halfField, oneAndHalfTile, mmTargetHeight, 90, 0, 90); + identifyTarget(3, "Blue Rear Wall", halfField, oneAndHalfTile, mmTargetHeight, 90, 0, -90); /* * Create a transformation matrix describing where the camera is on the robot. diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptWebcam.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptWebcam.java deleted file mode 100644 index 41f3dac577..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptWebcam.java +++ /dev/null @@ -1,310 +0,0 @@ -/* Copyright (c) 2020 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -import android.graphics.Bitmap; -import android.graphics.ImageFormat; -import android.os.Handler; - -import androidx.annotation.NonNull; - -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.robotcore.external.ClassFactory; -import org.firstinspires.ftc.robotcore.external.android.util.Size; -import org.firstinspires.ftc.robotcore.external.function.Consumer; -import org.firstinspires.ftc.robotcore.external.function.Continuation; -import org.firstinspires.ftc.robotcore.external.hardware.camera.Camera; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCaptureRequest; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCaptureSequenceId; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCaptureSession; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCharacteristics; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraException; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraFrame; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraManager; -import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; -import org.firstinspires.ftc.robotcore.internal.network.CallbackLooper; -import org.firstinspires.ftc.robotcore.internal.system.AppUtil; -import org.firstinspires.ftc.robotcore.internal.system.ContinuationSynchronizer; -import org.firstinspires.ftc.robotcore.internal.system.Deadline; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Locale; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * This OpMode illustrates how to open a webcam and retrieve images from it. It requires a configuration - * containing a webcam with the default name ("Webcam 1"). When the opmode runs, pressing the 'A' button - * will cause a frame from the camera to be written to a file on the device, which can then be retrieved - * by various means (e.g.: Device File Explorer in Android Studio; plugging the device into a PC and - * using Media Transfer; ADB; etc) - */ -@TeleOp(name="Concept: Webcam", group ="Concept") -@Disabled -public class ConceptWebcam extends LinearOpMode { - - //---------------------------------------------------------------------------------------------- - // State - //---------------------------------------------------------------------------------------------- - - private static final String TAG = "Webcam Sample"; - - /** How long we are to wait to be granted permission to use the camera before giving up. Here, - * we wait indefinitely */ - private static final int secondsPermissionTimeout = Integer.MAX_VALUE; - - /** State regarding our interaction with the camera */ - private CameraManager cameraManager; - private WebcamName cameraName; - private Camera camera; - private CameraCaptureSession cameraCaptureSession; - - /** The queue into which all frames from the camera are placed as they become available. - * Frames which are not processed by the OpMode are automatically discarded. */ - private EvictingBlockingQueue frameQueue; - - /** State regarding where and how to save frames when the 'A' button is pressed. */ - private int captureCounter = 0; - private File captureDirectory = AppUtil.ROBOT_DATA_DIR; - - /** A utility object that indicates where the asynchronous callbacks from the camera - * infrastructure are to run. In this OpMode, that's all hidden from you (but see {@link #startCamera} - * if you're curious): no knowledge of multi-threading is needed here. */ - private Handler callbackHandler; - - //---------------------------------------------------------------------------------------------- - // Main OpMode entry - //---------------------------------------------------------------------------------------------- - - @Override public void runOpMode() { - - callbackHandler = CallbackLooper.getDefault().getHandler(); - - cameraManager = ClassFactory.getInstance().getCameraManager(); - cameraName = hardwareMap.get(WebcamName.class, "Webcam 1"); - - initializeFrameQueue(2); - AppUtil.getInstance().ensureDirectoryExists(captureDirectory); - - try { - openCamera(); - if (camera == null) return; - - startCamera(); - if (cameraCaptureSession == null) return; - - telemetry.addData(">", "Press Play to start"); - telemetry.update(); - waitForStart(); - telemetry.clear(); - telemetry.addData(">", "Started...Press 'A' to capture frame"); - - boolean buttonPressSeen = false; - boolean captureWhenAvailable = false; - while (opModeIsActive()) { - - boolean buttonIsPressed = gamepad1.a; - if (buttonIsPressed && !buttonPressSeen) { - captureWhenAvailable = true; - } - buttonPressSeen = buttonIsPressed; - - if (captureWhenAvailable) { - Bitmap bmp = frameQueue.poll(); - if (bmp != null) { - captureWhenAvailable = false; - onNewFrame(bmp); - } - } - - telemetry.update(); - } - } finally { - closeCamera(); - } - } - - /** Do something with the frame */ - private void onNewFrame(Bitmap frame) { - saveBitmap(frame); - frame.recycle(); // not strictly necessary, but helpful - } - - //---------------------------------------------------------------------------------------------- - // Camera operations - //---------------------------------------------------------------------------------------------- - - private void initializeFrameQueue(int capacity) { - /** The frame queue will automatically throw away bitmap frames if they are not processed - * quickly by the OpMode. This avoids a buildup of frames in memory */ - frameQueue = new EvictingBlockingQueue(new ArrayBlockingQueue(capacity)); - frameQueue.setEvictAction(new Consumer() { - @Override public void accept(Bitmap frame) { - // RobotLog.ii(TAG, "frame recycled w/o processing"); - frame.recycle(); // not strictly necessary, but helpful - } - }); - } - - private void openCamera() { - if (camera != null) return; // be idempotent - - Deadline deadline = new Deadline(secondsPermissionTimeout, TimeUnit.SECONDS); - camera = cameraManager.requestPermissionAndOpenCamera(deadline, cameraName, null); - if (camera == null) { - error("camera not found or permission to use not granted: %s", cameraName); - } - } - - private void startCamera() { - if (cameraCaptureSession != null) return; // be idempotent - - /** YUY2 is supported by all Webcams, per the USB Webcam standard: See "USB Device Class Definition - * for Video Devices: Uncompressed Payload, Table 2-1". Further, often this is the *only* - * image format supported by a camera */ - final int imageFormat = ImageFormat.YUY2; - - /** Verify that the image is supported, and fetch size and desired frame rate if so */ - CameraCharacteristics cameraCharacteristics = cameraName.getCameraCharacteristics(); - if (!contains(cameraCharacteristics.getAndroidFormats(), imageFormat)) { - error("image format not supported"); - return; - } - final Size size = cameraCharacteristics.getDefaultSize(imageFormat); - final int fps = cameraCharacteristics.getMaxFramesPerSecond(imageFormat, size); - - /** Some of the logic below runs asynchronously on other threads. Use of the synchronizer - * here allows us to wait in this method until all that asynchrony completes before returning. */ - final ContinuationSynchronizer synchronizer = new ContinuationSynchronizer<>(); - try { - /** Create a session in which requests to capture frames can be made */ - camera.createCaptureSession(Continuation.create(callbackHandler, new CameraCaptureSession.StateCallbackDefault() { - @Override public void onConfigured(@NonNull CameraCaptureSession session) { - try { - /** The session is ready to go. Start requesting frames */ - final CameraCaptureRequest captureRequest = camera.createCaptureRequest(imageFormat, size, fps); - session.startCapture(captureRequest, - new CameraCaptureSession.CaptureCallback() { - @Override public void onNewFrame(@NonNull CameraCaptureSession session, @NonNull CameraCaptureRequest request, @NonNull CameraFrame cameraFrame) { - /** A new frame is available. The frame data has not been copied for us, and we can only access it - * for the duration of the callback. So we copy here manually. */ - Bitmap bmp = captureRequest.createEmptyBitmap(); - cameraFrame.copyToBitmap(bmp); - frameQueue.offer(bmp); - } - }, - Continuation.create(callbackHandler, new CameraCaptureSession.StatusCallback() { - @Override public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, CameraCaptureSequenceId cameraCaptureSequenceId, long lastFrameNumber) { - RobotLog.ii(TAG, "capture sequence %s reports completed: lastFrame=%d", cameraCaptureSequenceId, lastFrameNumber); - } - }) - ); - synchronizer.finish(session); - } catch (CameraException|RuntimeException e) { - RobotLog.ee(TAG, e, "exception starting capture"); - error("exception starting capture"); - session.close(); - synchronizer.finish(null); - } - } - })); - } catch (CameraException|RuntimeException e) { - RobotLog.ee(TAG, e, "exception starting camera"); - error("exception starting camera"); - synchronizer.finish(null); - } - - /** Wait for all the asynchrony to complete */ - try { - synchronizer.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - /** Retrieve the created session. This will be null on error. */ - cameraCaptureSession = synchronizer.getValue(); - } - - private void stopCamera() { - if (cameraCaptureSession != null) { - cameraCaptureSession.stopCapture(); - cameraCaptureSession.close(); - cameraCaptureSession = null; - } - } - - private void closeCamera() { - stopCamera(); - if (camera != null) { - camera.close(); - camera = null; - } - } - - //---------------------------------------------------------------------------------------------- - // Utilities - //---------------------------------------------------------------------------------------------- - - private void error(String msg) { - telemetry.log().add(msg); - telemetry.update(); - } - private void error(String format, Object...args) { - telemetry.log().add(format, args); - telemetry.update(); - } - - private boolean contains(int[] array, int value) { - for (int i : array) { - if (i == value) return true; - } - return false; - } - - private void saveBitmap(Bitmap bitmap) { - File file = new File(captureDirectory, String.format(Locale.getDefault(), "webcam-frame-%d.jpg", captureCounter++)); - try { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); - telemetry.log().add("captured %s", file.getName()); - } - } catch (IOException e) { - RobotLog.ee(TAG, e, "exception in saveBitmap()"); - error("exception saving %s", file.getName()); - } - } -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/HardwarePushbot.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/HardwarePushbot.java deleted file mode 100644 index 4848137e11..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/HardwarePushbot.java +++ /dev/null @@ -1,105 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -import com.qualcomm.robotcore.hardware.DcMotor; -import com.qualcomm.robotcore.hardware.HardwareMap; -import com.qualcomm.robotcore.hardware.Servo; -import com.qualcomm.robotcore.util.ElapsedTime; - -/** - * This is NOT an opmode. - * - * This class can be used to define all the specific hardware for a single robot. - * In this case that robot is a Pushbot. - * See PushbotTeleopTank_Iterative and others classes starting with "Pushbot" for usage examples. - * - * This hardware class assumes the following device names have been configured on the robot: - * Note: All names are lower case and some have single spaces between words. - * - * Motor channel: Left drive motor: "left_drive" - * Motor channel: Right drive motor: "right_drive" - * Motor channel: Manipulator drive motor: "left_arm" - * Servo channel: Servo to open left claw: "left_hand" - * Servo channel: Servo to open right claw: "right_hand" - */ -public class HardwarePushbot -{ - /* Public OpMode members. */ - public DcMotor leftDrive = null; - public DcMotor rightDrive = null; - public DcMotor leftArm = null; - public Servo leftClaw = null; - public Servo rightClaw = null; - - public static final double MID_SERVO = 0.5 ; - public static final double ARM_UP_POWER = 0.45 ; - public static final double ARM_DOWN_POWER = -0.45 ; - - /* local OpMode members. */ - HardwareMap hwMap = null; - private ElapsedTime period = new ElapsedTime(); - - /* Constructor */ - public HardwarePushbot(){ - - } - - /* Initialize standard Hardware interfaces */ - public void init(HardwareMap ahwMap) { - // Save reference to Hardware map - hwMap = ahwMap; - - // Define and Initialize Motors - leftDrive = hwMap.get(DcMotor.class, "left_drive"); - rightDrive = hwMap.get(DcMotor.class, "right_drive"); - leftArm = hwMap.get(DcMotor.class, "left_arm"); - leftDrive.setDirection(DcMotor.Direction.FORWARD); // Set to REVERSE if using AndyMark motors - rightDrive.setDirection(DcMotor.Direction.REVERSE);// Set to FORWARD if using AndyMark motors - - // Set all motors to zero power - leftDrive.setPower(0); - rightDrive.setPower(0); - leftArm.setPower(0); - - // Set all motors to run without encoders. - // May want to use RUN_USING_ENCODERS if encoders are installed. - leftDrive.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); - rightDrive.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); - leftArm.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER); - - // Define and initialize ALL installed servos. - leftClaw = hwMap.get(Servo.class, "left_hand"); - rightClaw = hwMap.get(Servo.class, "right_hand"); - leftClaw.setPosition(MID_SERVO); - rightClaw.setPosition(MID_SERVO); - } - } - diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByGyro_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByGyro_Linear.java deleted file mode 100644 index a366bec3e2..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByGyro_Linear.java +++ /dev/null @@ -1,363 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -import com.qualcomm.hardware.modernrobotics.ModernRoboticsI2cGyro; -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 com.qualcomm.robotcore.util.Range; - -/** - * This file illustrates the concept of driving a path based on Gyro heading and encoder counts. - * It uses the common Pushbot hardware class to define the drive on the robot. - * The code is structured as a LinearOpMode - * - * The code REQUIRES that you DO have encoders on the wheels, - * otherwise you would use: PushbotAutoDriveByTime; - * - * This code ALSO requires that you have a Modern Robotics I2C gyro with the name "gyro" - * otherwise you would use: PushbotAutoDriveByEncoder; - * - * This code requires that the drive Motors have been configured such that a positive - * power command moves them forward, and causes the encoders to count UP. - * - * This code uses the RUN_TO_POSITION mode to enable the Motor controllers to generate the run profile - * - * In order to calibrate the Gyro correctly, the robot must remain stationary during calibration. - * This is performed when the INIT button is pressed on the Driver Station. - * This code assumes that the robot is stationary when the INIT button is pressed. - * If this is not the case, then the INIT should be performed again. - * - * Note: in this example, all angles are referenced to the initial coordinate frame set during the - * the Gyro Calibration process, or whenever the program issues a resetZAxisIntegrator() call on the Gyro. - * - * The angle of movement/rotation is assumed to be a standardized rotation around the robot Z axis, - * which means that a Positive rotation is Counter Clock Wise, looking down on the field. - * This is consistent with the FTC field coordinate conventions set out in the document: - * ftc_app\doc\tutorial\FTC_FieldCoordinateSystemDefinition.pdf - * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list - */ - -@Autonomous(name="Pushbot: Auto Drive By Gyro", group="Pushbot") -@Disabled -public class PushbotAutoDriveByGyro_Linear extends LinearOpMode { - - /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // Use a Pushbot's hardware - ModernRoboticsI2cGyro gyro = null; // Additional Gyro device - - static final double COUNTS_PER_MOTOR_REV = 1440 ; // eg: TETRIX Motor Encoder - static final double DRIVE_GEAR_REDUCTION = 2.0 ; // This is < 1.0 if geared UP - static final double WHEEL_DIAMETER_INCHES = 4.0 ; // For figuring circumference - static final double COUNTS_PER_INCH = (COUNTS_PER_MOTOR_REV * DRIVE_GEAR_REDUCTION) / - (WHEEL_DIAMETER_INCHES * 3.1415); - - // These constants define the desired driving/control characteristics - // The can/should be tweaked to suite the specific robot drive train. - static final double DRIVE_SPEED = 0.7; // Nominal speed for better accuracy. - static final double TURN_SPEED = 0.5; // Nominal half speed for better accuracy. - - static final double HEADING_THRESHOLD = 1 ; // As tight as we can make it with an integer gyro - static final double P_TURN_COEFF = 0.1; // Larger is more responsive, but also less stable - static final double P_DRIVE_COEFF = 0.15; // Larger is more responsive, but also less stable - - - @Override - public void runOpMode() { - - /* - * Initialize the standard drive system variables. - * The init() method of the hardware class does most of the work here - */ - robot.init(hardwareMap); - gyro = (ModernRoboticsI2cGyro)hardwareMap.gyroSensor.get("gyro"); - - // Ensure the robot it stationary, then reset the encoders and calibrate the gyro. - robot.leftDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); - robot.rightDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); - - // Send telemetry message to alert driver that we are calibrating; - telemetry.addData(">", "Calibrating Gyro"); // - telemetry.update(); - - gyro.calibrate(); - - // make sure the gyro is calibrated before continuing - while (!isStopRequested() && gyro.isCalibrating()) { - sleep(50); - idle(); - } - - telemetry.addData(">", "Robot Ready."); // - telemetry.update(); - - robot.leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - robot.rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - - // Wait for the game to start (Display Gyro value), and reset gyro before we move.. - while (!isStarted()) { - telemetry.addData(">", "Robot Heading = %d", gyro.getIntegratedZValue()); - telemetry.update(); - } - - gyro.resetZAxisIntegrator(); - - // Step through each leg of the path, - // Note: Reverse movement is obtained by setting a negative distance (not speed) - // Put a hold after each turn - gyroDrive(DRIVE_SPEED, 48.0, 0.0); // Drive FWD 48 inches - gyroTurn( TURN_SPEED, -45.0); // Turn CCW to -45 Degrees - gyroHold( TURN_SPEED, -45.0, 0.5); // Hold -45 Deg heading for a 1/2 second - gyroDrive(DRIVE_SPEED, 12.0, -45.0); // Drive FWD 12 inches at 45 degrees - gyroTurn( TURN_SPEED, 45.0); // Turn CW to 45 Degrees - gyroHold( TURN_SPEED, 45.0, 0.5); // Hold 45 Deg heading for a 1/2 second - gyroTurn( TURN_SPEED, 0.0); // Turn CW to 0 Degrees - gyroHold( TURN_SPEED, 0.0, 1.0); // Hold 0 Deg heading for a 1 second - gyroDrive(DRIVE_SPEED,-48.0, 0.0); // Drive REV 48 inches - - telemetry.addData("Path", "Complete"); - telemetry.update(); - } - - - /** - * Method to drive on a fixed compass bearing (angle), based on encoder counts. - * Move will stop if either of these conditions occur: - * 1) Move gets to the desired position - * 2) Driver stops the opmode running. - * - * @param speed Target speed for forward motion. Should allow for _/- variance for adjusting heading - * @param distance Distance (in inches) to move from current position. Negative distance means move backwards. - * @param angle Absolute Angle (in Degrees) relative to last gyro reset. - * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. - * If a relative angle is required, add/subtract from current heading. - */ - public void gyroDrive ( double speed, - double distance, - double angle) { - - int newLeftTarget; - int newRightTarget; - int moveCounts; - double max; - double error; - double steer; - double leftSpeed; - double rightSpeed; - - // Ensure that the opmode is still active - if (opModeIsActive()) { - - // Determine new target position, and pass to motor controller - moveCounts = (int)(distance * COUNTS_PER_INCH); - newLeftTarget = robot.leftDrive.getCurrentPosition() + moveCounts; - newRightTarget = robot.rightDrive.getCurrentPosition() + moveCounts; - - // Set Target and Turn On RUN_TO_POSITION - robot.leftDrive.setTargetPosition(newLeftTarget); - robot.rightDrive.setTargetPosition(newRightTarget); - - robot.leftDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); - robot.rightDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); - - // start motion. - speed = Range.clip(Math.abs(speed), 0.0, 1.0); - robot.leftDrive.setPower(speed); - robot.rightDrive.setPower(speed); - - // keep looping while we are still active, and BOTH motors are running. - while (opModeIsActive() && - (robot.leftDrive.isBusy() && robot.rightDrive.isBusy())) { - - // adjust relative speed based on heading error. - error = getError(angle); - steer = getSteer(error, P_DRIVE_COEFF); - - // if driving in reverse, the motor correction also needs to be reversed - if (distance < 0) - steer *= -1.0; - - leftSpeed = speed - steer; - rightSpeed = speed + steer; - - // Normalize speeds if either one exceeds +/- 1.0; - max = Math.max(Math.abs(leftSpeed), Math.abs(rightSpeed)); - if (max > 1.0) - { - leftSpeed /= max; - rightSpeed /= max; - } - - robot.leftDrive.setPower(leftSpeed); - robot.rightDrive.setPower(rightSpeed); - - // Display drive status for the driver. - telemetry.addData("Err/St", "%5.1f/%5.1f", error, steer); - telemetry.addData("Target", "%7d:%7d", newLeftTarget, newRightTarget); - telemetry.addData("Actual", "%7d:%7d", robot.leftDrive.getCurrentPosition(), - robot.rightDrive.getCurrentPosition()); - telemetry.addData("Speed", "%5.2f:%5.2f", leftSpeed, rightSpeed); - telemetry.update(); - } - - // Stop all motion; - robot.leftDrive.setPower(0); - robot.rightDrive.setPower(0); - - // Turn off RUN_TO_POSITION - robot.leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - robot.rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - } - } - - /** - * Method to spin on central axis to point in a new direction. - * Move will stop if either of these conditions occur: - * 1) Move gets to the heading (angle) - * 2) Driver stops the opmode running. - * - * @param speed Desired speed of turn. - * @param angle Absolute Angle (in Degrees) relative to last gyro reset. - * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. - * If a relative angle is required, add/subtract from current heading. - */ - public void gyroTurn ( double speed, double angle) { - - // keep looping while we are still active, and not on heading. - while (opModeIsActive() && !onHeading(speed, angle, P_TURN_COEFF)) { - // Update telemetry & Allow time for other processes to run. - telemetry.update(); - } - } - - /** - * Method to obtain & hold a heading for a finite amount of time - * Move will stop once the requested time has elapsed - * - * @param speed Desired speed of turn. - * @param angle Absolute Angle (in Degrees) relative to last gyro reset. - * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. - * If a relative angle is required, add/subtract from current heading. - * @param holdTime Length of time (in seconds) to hold the specified heading. - */ - public void gyroHold( double speed, double angle, double holdTime) { - - ElapsedTime holdTimer = new ElapsedTime(); - - // keep looping while we have time remaining. - holdTimer.reset(); - while (opModeIsActive() && (holdTimer.time() < holdTime)) { - // Update telemetry & Allow time for other processes to run. - onHeading(speed, angle, P_TURN_COEFF); - telemetry.update(); - } - - // Stop all motion; - robot.leftDrive.setPower(0); - robot.rightDrive.setPower(0); - } - - /** - * Perform one cycle of closed loop heading control. - * - * @param speed Desired speed of turn. - * @param angle Absolute Angle (in Degrees) relative to last gyro reset. - * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. - * If a relative angle is required, add/subtract from current heading. - * @param PCoeff Proportional Gain coefficient - * @return - */ - boolean onHeading(double speed, double angle, double PCoeff) { - double error ; - double steer ; - boolean onTarget = false ; - double leftSpeed; - double rightSpeed; - - // determine turn power based on +/- error - error = getError(angle); - - if (Math.abs(error) <= HEADING_THRESHOLD) { - steer = 0.0; - leftSpeed = 0.0; - rightSpeed = 0.0; - onTarget = true; - } - else { - steer = getSteer(error, PCoeff); - rightSpeed = speed * steer; - leftSpeed = -rightSpeed; - } - - // Send desired speeds to motors. - robot.leftDrive.setPower(leftSpeed); - robot.rightDrive.setPower(rightSpeed); - - // Display it for the driver. - telemetry.addData("Target", "%5.2f", angle); - telemetry.addData("Err/St", "%5.2f/%5.2f", error, steer); - telemetry.addData("Speed.", "%5.2f:%5.2f", leftSpeed, rightSpeed); - - return onTarget; - } - - /** - * getError determines the error between the target angle and the robot's current heading - * @param targetAngle Desired angle (relative to global reference established at last Gyro Reset). - * @return error angle: Degrees in the range +/- 180. Centered on the robot's frame of reference - * +ve error means the robot should turn LEFT (CCW) to reduce error. - */ - public double getError(double targetAngle) { - - double robotError; - - // calculate error in -179 to +180 range ( - robotError = targetAngle - gyro.getIntegratedZValue(); - while (robotError > 180) robotError -= 360; - while (robotError <= -180) robotError += 360; - return robotError; - } - - /** - * returns desired steering force. +/- 1 range. +ve = steer left - * @param error Error angle in robot relative degrees - * @param PCoeff Proportional Gain Coefficient - * @return - */ - public double getSteer(double error, double PCoeff) { - return Range.clip(error * PCoeff, -1, 1); - } - -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveToLine_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveToLine_Linear.java deleted file mode 100644 index 2e174b1351..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveToLine_Linear.java +++ /dev/null @@ -1,118 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -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.LightSensor; - -/** - * This file illustrates the concept of driving up to a line and then stopping. - * It uses the common Pushbot hardware class to define the drive on the robot. - * The code is structured as a LinearOpMode - * - * The code shows using two different light sensors: - * The Primary sensor shown in this code is a legacy NXT Light sensor (called "sensor_light") - * Alternative "commented out" code uses a MR Optical Distance Sensor (called "sensor_ods") - * instead of the LEGO sensor. Chose to use one sensor or the other. - * - * Setting the correct WHITE_THRESHOLD value is key to stopping correctly. - * This should be set half way between the light and dark values. - * These values can be read on the screen once the OpMode has been INIT, but before it is STARTED. - * Move the senso on asnd off the white line and not the min and max readings. - * Edit this code to make WHITE_THRESHOLD half way between the min and max. - * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list - */ - -@Autonomous(name="Pushbot: Auto Drive To Line", group="Pushbot") -@Disabled -public class PushbotAutoDriveToLine_Linear extends LinearOpMode { - - /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // Use a Pushbot's hardware - LightSensor lightSensor; // Primary LEGO Light sensor, - // OpticalDistanceSensor lightSensor; // Alternative MR ODS sensor - - static final double WHITE_THRESHOLD = 0.2; // spans between 0.1 - 0.5 from dark to light - static final double APPROACH_SPEED = 0.5; - - @Override - public void runOpMode() { - - /* Initialize the drive system variables. - * The init() method of the hardware class does all the work here - */ - robot.init(hardwareMap); - - // If there are encoders connected, switch to RUN_USING_ENCODER mode for greater accuracy - // robot.leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - // robot.rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - - // get a reference to our Light Sensor object. - lightSensor = hardwareMap.lightSensor.get("sensor_light"); // Primary LEGO Light Sensor - // lightSensor = hardwareMap.opticalDistanceSensor.get("sensor_ods"); // Alternative MR ODS sensor. - - // turn on LED of light sensor. - lightSensor.enableLed(true); - - // Send telemetry message to signify robot waiting; - telemetry.addData("Status", "Ready to run"); // - telemetry.update(); - - // Wait for the game to start (driver presses PLAY) - // Abort this loop is started or stopped. - while (!(isStarted() || isStopRequested())) { - - // Display the light level while we are waiting to start - telemetry.addData("Light Level", lightSensor.getLightDetected()); - telemetry.update(); - idle(); - } - - // Start the robot moving forward, and then begin looking for a white line. - robot.leftDrive.setPower(APPROACH_SPEED); - robot.rightDrive.setPower(APPROACH_SPEED); - - // run until the white line is seen OR the driver presses STOP; - while (opModeIsActive() && (lightSensor.getLightDetected() < WHITE_THRESHOLD)) { - - // Display the light level while we are looking for the line - telemetry.addData("Light Level", lightSensor.getLightDetected()); - telemetry.update(); - } - - // Stop all motors - robot.leftDrive.setPower(0); - robot.rightDrive.setPower(0); - } -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByEncoder_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java similarity index 62% rename from FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByEncoder_Linear.java rename to FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java index facc53101a..ff7a878573 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByEncoder_Linear.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java @@ -33,45 +33,53 @@ 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.Servo; import com.qualcomm.robotcore.util.ElapsedTime; /** * This file illustrates the concept of driving a path based on encoder counts. - * It uses the common Pushbot hardware class to define the drive on the robot. * The code is structured as a LinearOpMode * * The code REQUIRES that you DO have encoders on the wheels, - * otherwise you would use: PushbotAutoDriveByTime; + * otherwise you would use: RobotAutoDriveByTime; * * This code ALSO requires that the drive Motors have been configured such that a positive - * power command moves them forwards, and causes the encoders to count UP. + * power command moves them forward, and causes the encoders to count UP. * * The desired path in this example is: * - Drive forward for 48 inches * - Spin right for 12 Inches - * - Drive Backwards for 24 inches + * - Drive Backward for 24 inches * - Stop and close the claw. * * The code is written using a method called: encoderDrive(speed, leftInches, rightInches, timeoutS) * that performs the actual movement. - * This methods assumes that each movement is relative to the last stopping place. + * This method assumes that each movement is relative to the last stopping place. * There are other ways to perform encoder based moves, but this method is probably the simplest. * This code uses the RUN_TO_POSITION mode to enable the Motor controllers to generate the run profile * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list */ -@Autonomous(name="Pushbot: Auto Drive By Encoder", group="Pushbot") +@Autonomous(name="Robot: Auto Drive By Encoder", group="Robot") @Disabled -public class PushbotAutoDriveByEncoder_Linear extends LinearOpMode { +public class RobotAutoDriveByEncoder_Linear extends LinearOpMode { /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // Use a Pushbot's hardware + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + private ElapsedTime runtime = new ElapsedTime(); + // Calculate the COUNTS_PER_INCH for your specific drive train. + // Go to your motor vendor website to determine your motor's COUNTS_PER_MOTOR_REV + // For external drive gearing, set DRIVE_GEAR_REDUCTION as needed. + // For example, use a value of 2.0 for a 12-tooth spur gear driving a 24-tooth spur gear. + // This is gearing DOWN for less speed and more torque. + // For gearing UP, use a gear ratio less than 1.0. Note this will affect the direction of wheel rotation. static final double COUNTS_PER_MOTOR_REV = 1440 ; // eg: TETRIX Motor Encoder - static final double DRIVE_GEAR_REDUCTION = 2.0 ; // This is < 1.0 if geared UP + static final double DRIVE_GEAR_REDUCTION = 1.0 ; // No External Gearing. static final double WHEEL_DIAMETER_INCHES = 4.0 ; // For figuring circumference static final double COUNTS_PER_INCH = (COUNTS_PER_MOTOR_REV * DRIVE_GEAR_REDUCTION) / (WHEEL_DIAMETER_INCHES * 3.1415); @@ -81,26 +89,26 @@ public class PushbotAutoDriveByEncoder_Linear extends LinearOpMode { @Override public void runOpMode() { - /* - * Initialize the drive system variables. - * The init() method of the hardware class does all the work here - */ - robot.init(hardwareMap); + // Initialize the drive system variables. + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); - // Send telemetry message to signify robot waiting; - telemetry.addData("Status", "Resetting Encoders"); // - telemetry.update(); + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // When run, this OpMode should start both motors driving forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); - robot.leftDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); - robot.rightDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); + leftDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); + rightDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); - robot.leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - robot.rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); // Send telemetry message to indicate successful Encoder reset - telemetry.addData("Path0", "Starting at %7d :%7d", - robot.leftDrive.getCurrentPosition(), - robot.rightDrive.getCurrentPosition()); + telemetry.addData("Starting at", "%7d :%7d", + leftDrive.getCurrentPosition(), + rightDrive.getCurrentPosition()); telemetry.update(); // Wait for the game to start (driver presses PLAY) @@ -112,12 +120,9 @@ public void runOpMode() { encoderDrive(TURN_SPEED, 12, -12, 4.0); // S2: Turn Right 12 Inches with 4 Sec timeout encoderDrive(DRIVE_SPEED, -24, -24, 4.0); // S3: Reverse 24 Inches with 4 Sec timeout - robot.leftClaw.setPosition(1.0); // S4: Stop and close the claw. - robot.rightClaw.setPosition(0.0); - sleep(1000); // pause for servos to move - telemetry.addData("Path", "Complete"); telemetry.update(); + sleep(1000); // pause to display final telemetry message. } /* @@ -138,19 +143,19 @@ public void encoderDrive(double speed, if (opModeIsActive()) { // Determine new target position, and pass to motor controller - newLeftTarget = robot.leftDrive.getCurrentPosition() + (int)(leftInches * COUNTS_PER_INCH); - newRightTarget = robot.rightDrive.getCurrentPosition() + (int)(rightInches * COUNTS_PER_INCH); - robot.leftDrive.setTargetPosition(newLeftTarget); - robot.rightDrive.setTargetPosition(newRightTarget); + newLeftTarget = leftDrive.getCurrentPosition() + (int)(leftInches * COUNTS_PER_INCH); + newRightTarget = rightDrive.getCurrentPosition() + (int)(rightInches * COUNTS_PER_INCH); + leftDrive.setTargetPosition(newLeftTarget); + rightDrive.setTargetPosition(newRightTarget); // Turn On RUN_TO_POSITION - robot.leftDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); - robot.rightDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); + leftDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); + rightDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); // reset the timeout time and start motion. runtime.reset(); - robot.leftDrive.setPower(Math.abs(speed)); - robot.rightDrive.setPower(Math.abs(speed)); + leftDrive.setPower(Math.abs(speed)); + rightDrive.setPower(Math.abs(speed)); // keep looping while we are still active, and there is time left, and both motors are running. // Note: We use (isBusy() && isBusy()) in the loop test, which means that when EITHER motor hits @@ -160,25 +165,24 @@ public void encoderDrive(double speed, // onto the next step, use (isBusy() || isBusy()) in the loop test. while (opModeIsActive() && (runtime.seconds() < timeoutS) && - (robot.leftDrive.isBusy() && robot.rightDrive.isBusy())) { + (leftDrive.isBusy() && rightDrive.isBusy())) { // Display it for the driver. - telemetry.addData("Path1", "Running to %7d :%7d", newLeftTarget, newRightTarget); - telemetry.addData("Path2", "Running at %7d :%7d", - robot.leftDrive.getCurrentPosition(), - robot.rightDrive.getCurrentPosition()); + telemetry.addData("Running to", " %7d :%7d", newLeftTarget, newRightTarget); + telemetry.addData("Currently at", " at %7d :%7d", + leftDrive.getCurrentPosition(), rightDrive.getCurrentPosition()); telemetry.update(); } // Stop all motion; - robot.leftDrive.setPower(0); - robot.rightDrive.setPower(0); + leftDrive.setPower(0); + rightDrive.setPower(0); // Turn off RUN_TO_POSITION - robot.leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - robot.rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); - // sleep(250); // optional pause after each move + sleep(250); // optional pause after each move. } } } diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByGyro_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByGyro_Linear.java new file mode 100644 index 0000000000..c710b21339 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByGyro_Linear.java @@ -0,0 +1,431 @@ +/* Copyright (c) 2022 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.hardware.bosch.BNO055IMU; +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 com.qualcomm.robotcore.util.Range; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; +import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; +import org.firstinspires.ftc.robotcore.external.navigation.Orientation; + +/** + * This file illustrates the concept of driving an autonomous path based on Gyro heading and encoder counts. + * The code is structured as a LinearOpMode + * + * The path to be followed by the robot is built from a series of drive, turn or pause steps. + * Each step on the path is defined by a single function call, and these can be strung together in any order. + * + * The code REQUIRES that you have encoders on the drive motors, otherwise you should use: RobotAutoDriveByTime; + * + * This code ALSO requires that you have a BOSCH BNO055 IMU, otherwise you would use: RobotAutoDriveByEncoder; + * This IMU is found in REV Control/Expansion Hubs shipped prior to July 2022, and possibly also on later models. + * To run as written, the Control/Expansion hub should be mounted horizontally on a flat part of the robot chassis. + * + * This sample requires that the drive Motors have been configured with names : left_drive and right_drive. + * It also requires that a positive power command moves both motors forward, and causes the encoders to count UP. + * So please verify that both of your motors move the robot forward on the first move. If not, make the required correction. + * See the beginning of runOpMode() to set the FORWARD/REVERSE option for each motor. + * + * This code uses RUN_TO_POSITION mode for driving straight, and RUN_USING_ENCODER mode for turning and holding. + * Note: You must call setTargetPosition() at least once before switching to RUN_TO_POSITION mode. + * + * Notes: + * + * All angles are referenced to the coordinate-frame that is set whenever resetHeading() is called. + * In this sample, the heading is reset when the Start button is touched on the Driver station. + * Note: It would be possible to reset the heading after each move, but this would accumulate steering errors. + * + * The angle of movement/rotation is assumed to be a standardized rotation around the robot Z axis, + * which means that a Positive rotation is Counter Clockwise, looking down on the field. + * This is consistent with the FTC field coordinate conventions set out in the document: + * ftc_app\doc\tutorial\FTC_FieldCoordinateSystemDefinition.pdf + * + * Control Approach. + * + * To reach, or maintain a required heading, this code implements a basic Proportional Controller where: + * + * Steering power = Heading Error * Proportional Gain. + * + * "Heading Error" is calculated by taking the difference between the desired heading and the actual heading, + * and then "normalizing" it by converting it to a value in the +/- 180 degree range. + * + * "Proportional Gain" is a constant that YOU choose to set the "strength" of the steering response. + * + * Use Android Studio to Copy this Class, and Paste it into your "TeamCode" folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list + */ + +@Autonomous(name="Robot: Auto Drive By Gyro", group="Robot") +@Disabled +public class RobotAutoDriveByGyro_Linear extends LinearOpMode { + + /* Declare OpMode members. */ + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + private BNO055IMU imu = null; // Control/Expansion Hub IMU + + private double robotHeading = 0; + private double headingOffset = 0; + private double headingError = 0; + + // These variable are declared here (as class members) so they can be updated in various methods, + // but still be displayed by sendTelemetry() + private double targetHeading = 0; + private double driveSpeed = 0; + private double turnSpeed = 0; + private double leftSpeed = 0; + private double rightSpeed = 0; + private int leftTarget = 0; + private int rightTarget = 0; + + // Calculate the COUNTS_PER_INCH for your specific drive train. + // Go to your motor vendor website to determine your motor's COUNTS_PER_MOTOR_REV + // For external drive gearing, set DRIVE_GEAR_REDUCTION as needed. + // For example, use a value of 2.0 for a 12-tooth spur gear driving a 24-tooth spur gear. + // This is gearing DOWN for less speed and more torque. + // For gearing UP, use a gear ratio less than 1.0. Note this will affect the direction of wheel rotation. + static final double COUNTS_PER_MOTOR_REV = 537.7 ; // eg: GoBILDA 312 RPM Yellow Jacket + static final double DRIVE_GEAR_REDUCTION = 1.0 ; // No External Gearing. + static final double WHEEL_DIAMETER_INCHES = 4.0 ; // For figuring circumference + static final double COUNTS_PER_INCH = (COUNTS_PER_MOTOR_REV * DRIVE_GEAR_REDUCTION) / + (WHEEL_DIAMETER_INCHES * 3.1415); + + // These constants define the desired driving/control characteristics + // They can/should be tweaked to suit the specific robot drive train. + static final double DRIVE_SPEED = 0.4; // Max driving speed for better distance accuracy. + static final double TURN_SPEED = 0.2; // Max Turn speed to limit turn rate + static final double HEADING_THRESHOLD = 1.0 ; // How close must the heading get to the target before moving to next step. + // Requiring more accuracy (a smaller number) will often make the turn take longer to get into the final position. + // Define the Proportional control coefficient (or GAIN) for "heading control". + // We define one value when Turning (larger errors), and the other is used when Driving straight (smaller errors). + // Increase these numbers if the heading does not corrects strongly enough (eg: a heavy robot or using tracks) + // Decrease these numbers if the heading does not settle on the correct value (eg: very agile robot with omni wheels) + static final double P_TURN_GAIN = 0.02; // Larger is more responsive, but also less stable + static final double P_DRIVE_GAIN = 0.03; // Larger is more responsive, but also less stable + + + @Override + public void runOpMode() { + + // Initialize the drive system variables. + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // When run, this OpMode should start both motors driving forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); + + // define initialization values for IMU, and then initialize it. + BNO055IMU.Parameters parameters = new BNO055IMU.Parameters(); + parameters.angleUnit = BNO055IMU.AngleUnit.DEGREES; + imu = hardwareMap.get(BNO055IMU.class, "imu"); + imu.initialize(parameters); + + // Ensure the robot is stationary. Reset the encoders and set the motors to BRAKE mode + leftDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); + rightDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); + leftDrive.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + rightDrive.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + + // Wait for the game to start (Display Gyro value while waiting) + while (opModeInInit()) { + telemetry.addData(">", "Robot Heading = %4.0f", getRawHeading()); + telemetry.update(); + } + + // Set the encoders for closed loop speed control, and reset the heading. + leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + resetHeading(); + + // Step through each leg of the path, + // Notes: Reverse movement is obtained by setting a negative distance (not speed) + // holdHeading() is used after turns to let the heading stabilize + // Add a sleep(2000) after any step to keep the telemetry data visible for review + + driveStraight(DRIVE_SPEED, 24.0, 0.0); // Drive Forward 24" + turnToHeading( TURN_SPEED, -45.0); // Turn CW to -45 Degrees + holdHeading( TURN_SPEED, -45.0, 0.5); // Hold -45 Deg heading for a 1/2 second + + driveStraight(DRIVE_SPEED, 17.0, -45.0); // Drive Forward 17" at -45 degrees (12"x and 12"y) + turnToHeading( TURN_SPEED, 45.0); // Turn CCW to 45 Degrees + holdHeading( TURN_SPEED, 45.0, 0.5); // Hold 45 Deg heading for a 1/2 second + + driveStraight(DRIVE_SPEED, 17.0, 45.0); // Drive Forward 17" at 45 degrees (-12"x and 12"y) + turnToHeading( TURN_SPEED, 0.0); // Turn CW to 0 Degrees + holdHeading( TURN_SPEED, 0.0, 1.0); // Hold 0 Deg heading for 1 second + + driveStraight(DRIVE_SPEED,-48.0, 0.0); // Drive in Reverse 48" (should return to approx. staring position) + + telemetry.addData("Path", "Complete"); + telemetry.update(); + sleep(1000); // Pause to display last telemetry message. + } + + /* + * ==================================================================================================== + * Driving "Helper" functions are below this line. + * These provide the high and low level methods that handle driving straight and turning. + * ==================================================================================================== + */ + + // ********** HIGH Level driving functions. ******************** + + /** + * Method to drive in a straight line, on a fixed compass heading (angle), based on encoder counts. + * Move will stop if either of these conditions occur: + * 1) Move gets to the desired position + * 2) Driver stops the opmode running. + * + * @param maxDriveSpeed MAX Speed for forward/rev motion (range 0 to +1.0) . + * @param distance Distance (in inches) to move from current position. Negative distance means move backward. + * @param heading Absolute Heading Angle (in Degrees) relative to last gyro reset. + * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. + * If a relative angle is required, add/subtract from the current robotHeading. + */ + public void driveStraight(double maxDriveSpeed, + double distance, + double heading) { + + // Ensure that the opmode is still active + if (opModeIsActive()) { + + // Determine new target position, and pass to motor controller + int moveCounts = (int)(distance * COUNTS_PER_INCH); + leftTarget = leftDrive.getCurrentPosition() + moveCounts; + rightTarget = rightDrive.getCurrentPosition() + moveCounts; + + // Set Target FIRST, then turn on RUN_TO_POSITION + leftDrive.setTargetPosition(leftTarget); + rightDrive.setTargetPosition(rightTarget); + + leftDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); + rightDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); + + // Set the required driving speed (must be positive for RUN_TO_POSITION) + // Start driving straight, and then enter the control loop + maxDriveSpeed = Math.abs(maxDriveSpeed); + moveRobot(maxDriveSpeed, 0); + + // keep looping while we are still active, and BOTH motors are running. + while (opModeIsActive() && + (leftDrive.isBusy() && rightDrive.isBusy())) { + + // Determine required steering to keep on heading + turnSpeed = getSteeringCorrection(heading, P_DRIVE_GAIN); + + // if driving in reverse, the motor correction also needs to be reversed + if (distance < 0) + turnSpeed *= -1.0; + + // Apply the turning correction to the current driving speed. + moveRobot(driveSpeed, turnSpeed); + + // Display drive status for the driver. + sendTelemetry(true); + } + + // Stop all motion & Turn off RUN_TO_POSITION + moveRobot(0, 0); + leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + } + } + + /** + * Method to spin on central axis to point in a new direction. + * Move will stop if either of these conditions occur: + * 1) Move gets to the heading (angle) + * 2) Driver stops the opmode running. + * + * @param maxTurnSpeed Desired MAX speed of turn. (range 0 to +1.0) + * @param heading Absolute Heading Angle (in Degrees) relative to last gyro reset. + * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. + * If a relative angle is required, add/subtract from current heading. + */ + public void turnToHeading(double maxTurnSpeed, double heading) { + + // Run getSteeringCorrection() once to pre-calculate the current error + getSteeringCorrection(heading, P_DRIVE_GAIN); + + // keep looping while we are still active, and not on heading. + while (opModeIsActive() && (Math.abs(headingError) > HEADING_THRESHOLD)) { + + // Determine required steering to keep on heading + turnSpeed = getSteeringCorrection(heading, P_TURN_GAIN); + + // Clip the speed to the maximum permitted value. + turnSpeed = Range.clip(turnSpeed, -maxTurnSpeed, maxTurnSpeed); + + // Pivot in place by applying the turning correction + moveRobot(0, turnSpeed); + + // Display drive status for the driver. + sendTelemetry(false); + } + + // Stop all motion; + moveRobot(0, 0); + } + + /** + * Method to obtain & hold a heading for a finite amount of time + * Move will stop once the requested time has elapsed + * This function is useful for giving the robot a moment to stabilize it's heading between movements. + * + * @param maxTurnSpeed Maximum differential turn speed (range 0 to +1.0) + * @param heading Absolute Heading Angle (in Degrees) relative to last gyro reset. + * 0 = fwd. +ve is CCW from fwd. -ve is CW from forward. + * If a relative angle is required, add/subtract from current heading. + * @param holdTime Length of time (in seconds) to hold the specified heading. + */ + public void holdHeading(double maxTurnSpeed, double heading, double holdTime) { + + ElapsedTime holdTimer = new ElapsedTime(); + holdTimer.reset(); + + // keep looping while we have time remaining. + while (opModeIsActive() && (holdTimer.time() < holdTime)) { + // Determine required steering to keep on heading + turnSpeed = getSteeringCorrection(heading, P_TURN_GAIN); + + // Clip the speed to the maximum permitted value. + turnSpeed = Range.clip(turnSpeed, -maxTurnSpeed, maxTurnSpeed); + + // Pivot in place by applying the turning correction + moveRobot(0, turnSpeed); + + // Display drive status for the driver. + sendTelemetry(false); + } + + // Stop all motion; + moveRobot(0, 0); + } + + // ********** LOW Level driving functions. ******************** + + /** + * This method uses a Proportional Controller to determine how much steering correction is required. + * + * @param desiredHeading The desired absolute heading (relative to last heading reset) + * @param proportionalGain Gain factor applied to heading error to obtain turning power. + * @return Turning power needed to get to required heading. + */ + public double getSteeringCorrection(double desiredHeading, double proportionalGain) { + targetHeading = desiredHeading; // Save for telemetry + + // Get the robot heading by applying an offset to the IMU heading + robotHeading = getRawHeading() - headingOffset; + + // Determine the heading current error + headingError = targetHeading - robotHeading; + + // Normalize the error to be within +/- 180 degrees + while (headingError > 180) headingError -= 360; + while (headingError <= -180) headingError += 360; + + // Multiply the error by the gain to determine the required steering correction/ Limit the result to +/- 1.0 + return Range.clip(headingError * proportionalGain, -1, 1); + } + + /** + * This method takes separate drive (fwd/rev) and turn (right/left) requests, + * combines them, and applies the appropriate speed commands to the left and right wheel motors. + * @param drive forward motor speed + * @param turn clockwise turning motor speed. + */ + public void moveRobot(double drive, double turn) { + driveSpeed = drive; // save this value as a class member so it can be used by telemetry. + turnSpeed = turn; // save this value as a class member so it can be used by telemetry. + + leftSpeed = drive - turn; + rightSpeed = drive + turn; + + // Scale speeds down if either one exceeds +/- 1.0; + double max = Math.max(Math.abs(leftSpeed), Math.abs(rightSpeed)); + if (max > 1.0) + { + leftSpeed /= max; + rightSpeed /= max; + } + + leftDrive.setPower(leftSpeed); + rightDrive.setPower(rightSpeed); + } + + /** + * Display the various control parameters while driving + * + * @param straight Set to true if we are driving straight, and the encoder positions should be included in the telemetry. + */ + private void sendTelemetry(boolean straight) { + + if (straight) { + telemetry.addData("Motion", "Drive Straight"); + telemetry.addData("Target Pos L:R", "%7d:%7d", leftTarget, rightTarget); + telemetry.addData("Actual Pos L:R", "%7d:%7d", leftDrive.getCurrentPosition(), + rightDrive.getCurrentPosition()); + } else { + telemetry.addData("Motion", "Turning"); + } + + telemetry.addData("Angle Target:Current", "%5.2f:%5.0f", targetHeading, robotHeading); + telemetry.addData("Error:Steer", "%5.1f:%5.1f", headingError, turnSpeed); + telemetry.addData("Wheel Speeds L:R.", "%5.2f : %5.2f", leftSpeed, rightSpeed); + telemetry.update(); + } + + /** + * read the raw (un-offset Gyro heading) directly from the IMU + */ + public double getRawHeading() { + Orientation angles = imu.getAngularOrientation(AxesReference.INTRINSIC, AxesOrder.ZYX, AngleUnit.DEGREES); + return angles.firstAngle; + } + + /** + * Reset the "offset" heading back to zero + */ + public void resetHeading() { + // Save a new heading offset equal to the current raw heading. + headingOffset = getRawHeading(); + robotHeading = 0; + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByTime_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java similarity index 67% rename from FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByTime_Linear.java rename to FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java index a5bde9db9e..e27c9c7550 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotAutoDriveByTime_Linear.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java @@ -32,35 +32,36 @@ 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; /** * This file illustrates the concept of driving a path based on time. - * It uses the common Pushbot hardware class to define the drive on the robot. * The code is structured as a LinearOpMode * * The code assumes that you do NOT have encoders on the wheels, - * otherwise you would use: PushbotAutoDriveByEncoder; + * otherwise you would use: RobotAutoDriveByEncoder; * * The desired path in this example is: * - Drive forward for 3 seconds * - Spin right for 1.3 seconds - * - Drive Backwards for 1 Second - * - Stop and close the claw. + * - Drive Backward for 1 Second * * The code is written in a simple form with no optimizations. * However, there are several ways that this type of sequence could be streamlined, * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list */ -@Autonomous(name="Pushbot: Auto Drive By Time", group="Pushbot") +@Autonomous(name="Robot: Auto Drive By Time", group="Robot") @Disabled -public class PushbotAutoDriveByTime_Linear extends LinearOpMode { +public class RobotAutoDriveByTime_Linear extends LinearOpMode { /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // Use a Pushbot's hardware + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + private ElapsedTime runtime = new ElapsedTime(); @@ -70,11 +71,15 @@ public class PushbotAutoDriveByTime_Linear extends LinearOpMode { @Override public void runOpMode() { - /* - * Initialize the drive system variables. - * The init() method of the hardware class does all the work here - */ - robot.init(hardwareMap); + // Initialize the drive system variables. + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // When run, this OpMode should start both motors driving forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); // Send telemetry message to signify robot waiting; telemetry.addData("Status", "Ready to run"); // @@ -86,37 +91,35 @@ public void runOpMode() { // Step through each leg of the path, ensuring that the Auto mode has not been stopped along the way // Step 1: Drive forward for 3 seconds - robot.leftDrive.setPower(FORWARD_SPEED); - robot.rightDrive.setPower(FORWARD_SPEED); + leftDrive.setPower(FORWARD_SPEED); + rightDrive.setPower(FORWARD_SPEED); runtime.reset(); while (opModeIsActive() && (runtime.seconds() < 3.0)) { - telemetry.addData("Path", "Leg 1: %2.5f S Elapsed", runtime.seconds()); + telemetry.addData("Path", "Leg 1: %4.1f S Elapsed", runtime.seconds()); telemetry.update(); } // Step 2: Spin right for 1.3 seconds - robot.leftDrive.setPower(TURN_SPEED); - robot.rightDrive.setPower(-TURN_SPEED); + leftDrive.setPower(TURN_SPEED); + rightDrive.setPower(-TURN_SPEED); runtime.reset(); while (opModeIsActive() && (runtime.seconds() < 1.3)) { - telemetry.addData("Path", "Leg 2: %2.5f S Elapsed", runtime.seconds()); + telemetry.addData("Path", "Leg 2: %4.1f S Elapsed", runtime.seconds()); telemetry.update(); } - // Step 3: Drive Backwards for 1 Second - robot.leftDrive.setPower(-FORWARD_SPEED); - robot.rightDrive.setPower(-FORWARD_SPEED); + // Step 3: Drive Backward for 1 Second + leftDrive.setPower(-FORWARD_SPEED); + rightDrive.setPower(-FORWARD_SPEED); runtime.reset(); while (opModeIsActive() && (runtime.seconds() < 1.0)) { - telemetry.addData("Path", "Leg 3: %2.5f S Elapsed", runtime.seconds()); + telemetry.addData("Path", "Leg 3: %4.1f S Elapsed", runtime.seconds()); telemetry.update(); } - // Step 4: Stop and close the claw. - robot.leftDrive.setPower(0); - robot.rightDrive.setPower(0); - robot.leftClaw.setPosition(1.0); - robot.rightClaw.setPosition(0.0); + // Step 4: Stop + leftDrive.setPower(0); + rightDrive.setPower(0); telemetry.addData("Path", "Complete"); telemetry.update(); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToLine_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToLine_Linear.java new file mode 100644 index 0000000000..9ddf6ad093 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToLine_Linear.java @@ -0,0 +1,144 @@ +/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; + +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.ColorSensor; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.LightSensor; +import com.qualcomm.robotcore.hardware.NormalizedColorSensor; +import com.qualcomm.robotcore.hardware.NormalizedRGBA; +import com.qualcomm.robotcore.hardware.SwitchableLight; + +/** + * This file illustrates the concept of driving up to a line and then stopping. + * The code is structured as a LinearOpMode + * + * The Sensor used here can be a REV Color Sensor V2 or V3. Make sure the white LED is turned on. + * The sensor can be plugged into any I2C port, and must be named "sensor_color" in the active configuration. + * + * Depending on the height of your color sensor, you may want to set the sensor "gain". + * The higher the gain, the greater the reflected light reading will be. + * Use the SensorColor sample in this folder to determine the minimum gain value that provides an + * "Alpha" reading of 1.0 when you are on top of the white line. In this sample, we use a gain of 15 + * which works well with a Rev V2 color sensor + * + * Setting the correct WHITE_THRESHOLD value is key to stopping correctly. + * This should be set halfway between the bare-tile, and white-line "Alpha" values. + * The reflected light value can be read on the screen once the OpMode has been INIT, but before it is STARTED. + * Move the sensor on and off the white line and note the min and max readings. + * Edit this code to make WHITE_THRESHOLD halfway between the min and max. + * + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list + */ + +@Autonomous(name="Robot: Auto Drive To Line", group="Robot") +@Disabled +public class RobotAutoDriveToLine_Linear extends LinearOpMode { + + /* Declare OpMode members. */ + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + + /** The colorSensor field will contain a reference to our color sensor hardware object */ + NormalizedColorSensor colorSensor; + + static final double WHITE_THRESHOLD = 0.5; // spans between 0.0 - 1.0 from dark to light + static final double APPROACH_SPEED = 0.25; + + @Override + public void runOpMode() { + + // Initialize the drive system variables. + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // When run, this OpMode should start both motors driving forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); + + // If there are encoders connected, switch to RUN_USING_ENCODER mode for greater accuracy + // leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + // rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + // Get a reference to our sensor object. It's recommended to use NormalizedColorSensor over + // ColorSensor, because NormalizedColorSensor consistently gives values between 0 and 1, while + // the values you get from ColorSensor are dependent on the specific sensor you're using. + colorSensor = hardwareMap.get(NormalizedColorSensor.class, "sensor_color"); + + // If necessary, turn ON the white LED (if there is no LED switch on the sensor) + if (colorSensor instanceof SwitchableLight) { + ((SwitchableLight)colorSensor).enableLight(true); + } + + // Some sensors allow you to set your light sensor gain for optimal sensitivity... + // See the SensorColor sample in this folder for how to determine the optimal gain. + // A gain of 15 causes a Rev Color Sensor V2 to produce an Alpha value of 1.0 at about 1.5" above the floor. + colorSensor.setGain(15); + + // Wait for driver to press PLAY) + // Abort this loop is started or stopped. + while (opModeInInit()) { + + // Send telemetry message to signify robot waiting; + telemetry.addData("Status", "Ready to drive to white line."); // + + // Display the light level while we are waiting to start + getBrightness(); + } + + // Start the robot moving forward, and then begin looking for a white line. + leftDrive.setPower(APPROACH_SPEED); + rightDrive.setPower(APPROACH_SPEED); + + // run until the white line is seen OR the driver presses STOP; + while (opModeIsActive() && (getBrightness() < WHITE_THRESHOLD)) { + sleep(5); + } + + // Stop all motors + leftDrive.setPower(0); + rightDrive.setPower(0); + } + + // to obtain reflected light, read the normalized values from the color sensor. Return the Alpha channel. + double getBrightness() { + NormalizedRGBA colors = colorSensor.getNormalizedColors(); + telemetry.addData("Light Level (0 to 1)", "%4.2f", colors.alpha); + telemetry.update(); + + return colors.alpha; + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotHardware.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotHardware.java new file mode 100644 index 0000000000..71b8e0c966 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotHardware.java @@ -0,0 +1,167 @@ +/* Copyright (c) 2022 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.Servo; +import com.qualcomm.robotcore.util.Range; + +/** + * This file works in conjunction with the External Hardware Class sample called: ConceptExternalHardwareClass.java + * Please read the explanations in that Sample about how to use this class definition. + * + * This file defines a Java Class that performs all the setup and configuration for a sample robot's hardware (motors and sensors). + * It assumes three motors (left_drive, right_drive and arm) and two servos (left_hand and right_hand) + * + * This one file/class can be used by ALL of your OpModes without having to cut & paste the code each time. + * + * Where possible, the actual hardware objects are "abstracted" (or hidden) so the OpMode code just makes calls into the class, + * rather than accessing the internal hardware directly. This is why the objects are declared "private". + * + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with *exactly the same name*. + * + * Or.. In OnBot Java, add a new file named RobotHardware.java, drawing from this Sample; select Not an OpMode. + * Also add a new OpMode, drawing from the Sample ConceptExternalHardwareClass.java; select TeleOp. + * + */ + +public class RobotHardware { + + /* Declare OpMode members. */ + private LinearOpMode myOpMode = null; // gain access to methods in the calling OpMode. + + // Define Motor and Servo objects (Make them private so they can't be accessed externally) + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + private DcMotor armMotor = null; + private Servo leftHand = null; + private Servo rightHand = null; + + // Define Drive constants. Make them public so they CAN be used by the calling OpMode + public static final double MID_SERVO = 0.5 ; + public static final double HAND_SPEED = 0.02 ; // sets rate to move servo + public static final double ARM_UP_POWER = 0.45 ; + public static final double ARM_DOWN_POWER = -0.45 ; + + // Define a constructor that allows the OpMode to pass a reference to itself. + public RobotHardware (LinearOpMode opmode) { + myOpMode = opmode; + } + + /** + * Initialize all the robot's hardware. + * This method must be called ONCE when the OpMode is initialized. + * + * All of the hardware devices are accessed via the hardware map, and initialized. + */ + public void init() { + // Define and Initialize Motors (note: need to use reference to actual OpMode). + leftDrive = myOpMode.hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = myOpMode.hardwareMap.get(DcMotor.class, "right_drive"); + armMotor = myOpMode.hardwareMap.get(DcMotor.class, "arm"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // Pushing the left stick forward MUST make robot go forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); + + // If there are encoders connected, switch to RUN_USING_ENCODER mode for greater accuracy + // leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + // rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + // Define and initialize ALL installed servos. + leftHand = myOpMode.hardwareMap.get(Servo.class, "left_hand"); + rightHand = myOpMode.hardwareMap.get(Servo.class, "right_hand"); + leftHand.setPosition(MID_SERVO); + rightHand.setPosition(MID_SERVO); + + myOpMode.telemetry.addData(">", "Hardware Initialized"); + myOpMode.telemetry.update(); + } + + /** + * Calculates the left/right motor powers required to achieve the requested + * robot motions: Drive (Axial motion) and Turn (Yaw motion). + * Then sends these power levels to the motors. + * + * @param Drive Fwd/Rev driving power (-1.0 to 1.0) +ve is forward + * @param Turn Right/Left turning power (-1.0 to 1.0) +ve is CW + */ + public void driveRobot(double Drive, double Turn) { + // Combine drive and turn for blended motion. + double left = Drive + Turn; + double right = Drive - Turn; + + // Scale the values so neither exceed +/- 1.0 + double max = Math.max(Math.abs(left), Math.abs(right)); + if (max > 1.0) + { + left /= max; + right /= max; + } + + // Use existing function to drive both wheels. + setDrivePower(left, right); + } + + /** + * Pass the requested wheel motor powers to the appropriate hardware drive motors. + * + * @param leftWheel Fwd/Rev driving power (-1.0 to 1.0) +ve is forward + * @param rightWheel Fwd/Rev driving power (-1.0 to 1.0) +ve is forward + */ + public void setDrivePower(double leftWheel, double rightWheel) { + // Output the values to the motor drives. + leftDrive.setPower(leftWheel); + rightDrive.setPower(rightWheel); + } + + /** + * Pass the requested arm power to the appropriate hardware drive motor + * + * @param power driving power (-1.0 to 1.0) + */ + public void setArmPower(double power) { + armMotor.setPower(power); + } + + /** + * Send the two hand-servos to opposing (mirrored) positions, based on the passed offset. + * + * @param offset + */ + public void setHandPositions(double offset) { + offset = Range.clip(offset, -0.5, 0.5); + leftHand.setPosition(MID_SERVO + offset); + rightHand.setPosition(MID_SERVO - offset); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotTeleopPOV_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java similarity index 63% rename from FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotTeleopPOV_Linear.java rename to FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java index cc168d8c61..d4fac1def8 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotTeleopPOV_Linear.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java @@ -32,30 +32,39 @@ 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.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.Servo; import com.qualcomm.robotcore.util.Range; /** - * This OpMode uses the common Pushbot hardware class to define the devices on the robot. - * All device access is managed through the HardwarePushbot class. + * This particular OpMode executes a POV Game style Teleop for a direct drive robot * The code is structured as a LinearOpMode * - * This particular OpMode executes a POV Game style Teleop for a PushBot * In this mode the left stick moves the robot FWD and back, the Right stick turns left and right. - * It raises and lowers the claw using the Gampad Y and A buttons respectively. + * It raises and lowers the arm using the Gamepad Y and A buttons respectively. * It also opens and closes the claws slowly using the left and right Bumper buttons. * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list */ -@TeleOp(name="Pushbot: Teleop POV", group="Pushbot") +@TeleOp(name="Robot: Teleop POV", group="Robot") @Disabled -public class PushbotTeleopPOV_Linear extends LinearOpMode { +public class RobotTeleopPOV_Linear extends LinearOpMode { /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // Use a Pushbot's hardware - double clawOffset = 0; // Servo mid position - final double CLAW_SPEED = 0.02 ; // sets rate to move servo + public DcMotor leftDrive = null; + public DcMotor rightDrive = null; + public DcMotor leftArm = null; + public Servo leftClaw = null; + public Servo rightClaw = null; + + double clawOffset = 0; + + public static final double MID_SERVO = 0.5 ; + public static final double CLAW_SPEED = 0.02 ; // sets rate to move servo + public static final double ARM_UP_POWER = 0.45 ; + public static final double ARM_DOWN_POWER = -0.45 ; @Override public void runOpMode() { @@ -65,13 +74,29 @@ public void runOpMode() { double turn; double max; - /* Initialize the hardware variables. - * The init() method of the hardware class does all the work here - */ - robot.init(hardwareMap); + // Define and Initialize Motors + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); + leftArm = hardwareMap.get(DcMotor.class, "left_arm"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // Pushing the left stick forward MUST make robot go forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); + + // If there are encoders connected, switch to RUN_USING_ENCODER mode for greater accuracy + // leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + // rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + // Define and initialize ALL installed servos. + leftClaw = hardwareMap.get(Servo.class, "left_hand"); + rightClaw = hardwareMap.get(Servo.class, "right_hand"); + leftClaw.setPosition(MID_SERVO); + rightClaw.setPosition(MID_SERVO); // Send telemetry message to signify robot waiting; - telemetry.addData("Say", "Hello Driver"); // + telemetry.addData(">", "Robot Ready. Press Play."); // telemetry.update(); // Wait for the game to start (driver presses PLAY) @@ -80,7 +105,7 @@ public void runOpMode() { // run until the end of the match (driver presses STOP) while (opModeIsActive()) { - // Run wheels in POV mode (note: The joystick goes negative when pushed forwards, so negate it) + // Run wheels in POV mode (note: The joystick goes negative when pushed forward, so negate it) // In this mode the Left stick moves the robot fwd and back, the Right stick turns left and right. // This way it's also easy to just drive straight, or just turn. drive = -gamepad1.left_stick_y; @@ -99,8 +124,8 @@ public void runOpMode() { } // Output the safe vales to the motor drives. - robot.leftDrive.setPower(left); - robot.rightDrive.setPower(right); + leftDrive.setPower(left); + rightDrive.setPower(right); // Use gamepad left & right Bumpers to open and close the claw if (gamepad1.right_bumper) @@ -110,16 +135,16 @@ else if (gamepad1.left_bumper) // Move both servos to new position. Assume servos are mirror image of each other. clawOffset = Range.clip(clawOffset, -0.5, 0.5); - robot.leftClaw.setPosition(robot.MID_SERVO + clawOffset); - robot.rightClaw.setPosition(robot.MID_SERVO - clawOffset); + leftClaw.setPosition(MID_SERVO + clawOffset); + rightClaw.setPosition(MID_SERVO - clawOffset); // Use gamepad buttons to move arm up (Y) and down (A) if (gamepad1.y) - robot.leftArm.setPower(robot.ARM_UP_POWER); + leftArm.setPower(ARM_UP_POWER); else if (gamepad1.a) - robot.leftArm.setPower(robot.ARM_DOWN_POWER); + leftArm.setPower(ARM_DOWN_POWER); else - robot.leftArm.setPower(0.0); + leftArm.setPower(0.0); // Send telemetry message to signify robot running; telemetry.addData("claw", "Offset = %.2f", clawOffset); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotTeleopTank_Iterative.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java similarity index 56% rename from FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotTeleopTank_Iterative.java rename to FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java index ec336a8478..9f1aa7cb8d 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/PushbotTeleopTank_Iterative.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java @@ -32,44 +32,69 @@ import com.qualcomm.robotcore.eventloop.opmode.Disabled; import com.qualcomm.robotcore.eventloop.opmode.OpMode; import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.Servo; import com.qualcomm.robotcore.util.Range; /** - * This file provides basic Telop driving for a Pushbot robot. + * This particular OpMode executes a Tank Drive control TeleOp a direct drive robot * The code is structured as an Iterative OpMode * - * This OpMode uses the common Pushbot hardware class to define the devices on the robot. - * All device access is managed through the HardwarePushbot class. - * - * This particular OpMode executes a basic Tank Drive Teleop for a PushBot - * It raises and lowers the claw using the Gampad Y and A buttons respectively. + * In this mode, the left and right joysticks control the left and right motors respectively. + * Pushing a joystick forward will make the attached motor drive forward. + * It raises and lowers the claw using the Gamepad Y and A buttons respectively. * It also opens and closes the claws slowly using the left and right Bumper buttons. * - * Use Android Studios to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list */ -@TeleOp(name="Pushbot: Teleop Tank", group="Pushbot") +@TeleOp(name="Robot: Teleop Tank", group="Robot") @Disabled -public class PushbotTeleopTank_Iterative extends OpMode{ +public class RobotTeleopTank_Iterative extends OpMode{ /* Declare OpMode members. */ - HardwarePushbot robot = new HardwarePushbot(); // use the class created to define a Pushbot's hardware - double clawOffset = 0.0 ; // Servo mid position - final double CLAW_SPEED = 0.02 ; // sets rate to move servo + public DcMotor leftDrive = null; + public DcMotor rightDrive = null; + public DcMotor leftArm = null; + public Servo leftClaw = null; + public Servo rightClaw = null; + + double clawOffset = 0; + + public static final double MID_SERVO = 0.5 ; + public static final double CLAW_SPEED = 0.02 ; // sets rate to move servo + public static final double ARM_UP_POWER = 0.50 ; // Run arm motor up at 50% power + public static final double ARM_DOWN_POWER = -0.25 ; // Run arm motor down at -25% power /* * Code to run ONCE when the driver hits INIT */ @Override public void init() { - /* Initialize the hardware variables. - * The init() method of the hardware class does all the work here - */ - robot.init(hardwareMap); + // Define and Initialize Motors + leftDrive = hardwareMap.get(DcMotor.class, "left_drive"); + rightDrive = hardwareMap.get(DcMotor.class, "right_drive"); + leftArm = hardwareMap.get(DcMotor.class, "left_arm"); + + // To drive forward, most robots need the motor on one side to be reversed, because the axles point in opposite directions. + // Pushing the left and right sticks forward MUST make robot go forward. So adjust these two lines based on your first test drive. + // Note: The settings here assume direct drive on left and right wheels. Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); + + // If there are encoders connected, switch to RUN_USING_ENCODER mode for greater accuracy + // leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + // rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + // Define and initialize ALL installed servos. + leftClaw = hardwareMap.get(Servo.class, "left_hand"); + rightClaw = hardwareMap.get(Servo.class, "right_hand"); + leftClaw.setPosition(MID_SERVO); + rightClaw.setPosition(MID_SERVO); // Send telemetry message to signify robot waiting; - telemetry.addData("Say", "Hello Driver"); // + telemetry.addData(">", "Robot Ready. Press Play."); // } /* @@ -94,12 +119,12 @@ public void loop() { double left; double right; - // Run wheels in tank mode (note: The joystick goes negative when pushed forwards, so negate it) + // Run wheels in tank mode (note: The joystick goes negative when pushed forward, so negate it) left = -gamepad1.left_stick_y; right = -gamepad1.right_stick_y; - robot.leftDrive.setPower(left); - robot.rightDrive.setPower(right); + leftDrive.setPower(left); + rightDrive.setPower(right); // Use gamepad left & right Bumpers to open and close the claw if (gamepad1.right_bumper) @@ -109,16 +134,16 @@ else if (gamepad1.left_bumper) // Move both servos to new position. Assume servos are mirror image of each other. clawOffset = Range.clip(clawOffset, -0.5, 0.5); - robot.leftClaw.setPosition(robot.MID_SERVO + clawOffset); - robot.rightClaw.setPosition(robot.MID_SERVO - clawOffset); + leftClaw.setPosition(MID_SERVO + clawOffset); + rightClaw.setPosition(MID_SERVO - clawOffset); // Use gamepad buttons to move the arm up (Y) and down (A) if (gamepad1.y) - robot.leftArm.setPower(robot.ARM_UP_POWER); + leftArm.setPower(ARM_UP_POWER); else if (gamepad1.a) - robot.leftArm.setPower(robot.ARM_DOWN_POWER); + leftArm.setPower(ARM_DOWN_POWER); else - robot.leftArm.setPower(0.0); + leftArm.setPower(0.0); // Send telemetry message to signify robot running; telemetry.addData("claw", "Offset = %.2f", clawOffset); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAdafruitRGB.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAdafruitRGB.java deleted file mode 100644 index 6a94e25fb6..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAdafruitRGB.java +++ /dev/null @@ -1,167 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -import android.app.Activity; -import android.graphics.Color; -import android.view.View; - -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.hardware.ColorSensor; -import com.qualcomm.robotcore.hardware.DeviceInterfaceModule; -import com.qualcomm.robotcore.hardware.DigitalChannel; - -/* - * - * This is an example LinearOpMode that shows how to use - * the Adafruit RGB Sensor. It assumes that the I2C - * cable for the sensor is connected to an I2C port on the - * Core Device Interface Module. - * - * It also assuems that the LED pin of the sensor is connected - * to the digital signal pin of a digital port on the - * Core Device Interface Module. - * - * You can use the digital port to turn the sensor's onboard - * LED on or off. - * - * The op mode assumes that the Core Device Interface Module - * is configured with a name of "dim" and that the Adafruit color sensor - * is configured as an I2C device with a name of "sensor_color". - * - * It also assumes that the LED pin of the RGB sensor - * is connected to the signal pin of digital port #5 (zero indexed) - * of the Core Device Interface Module. - * - * You can use the X button on gamepad1 to toggle the LED on and off. - * - * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list - */ -@TeleOp(name = "Sensor: AdafruitRGB", group = "Sensor") -@Disabled // Comment this out to add to the opmode list -public class SensorAdafruitRGB extends LinearOpMode { - - ColorSensor sensorRGB; - DeviceInterfaceModule cdim; - - // we assume that the LED pin of the RGB sensor is connected to - // digital port 5 (zero indexed). - static final int LED_CHANNEL = 5; - - @Override - public void runOpMode() { - - // hsvValues is an array that will hold the hue, saturation, and value information. - float hsvValues[] = {0F,0F,0F}; - - // values is a reference to the hsvValues array. - final float values[] = hsvValues; - - // get a reference to the RelativeLayout so we can change the background - // color of the Robot Controller app to match the hue detected by the RGB sensor. - int relativeLayoutId = hardwareMap.appContext.getResources().getIdentifier("RelativeLayout", "id", hardwareMap.appContext.getPackageName()); - final View relativeLayout = ((Activity) hardwareMap.appContext).findViewById(relativeLayoutId); - - // bPrevState and bCurrState represent the previous and current state of the button. - boolean bPrevState = false; - boolean bCurrState = false; - - // bLedOn represents the state of the LED. - boolean bLedOn = true; - - // get a reference to our DeviceInterfaceModule object. - cdim = hardwareMap.deviceInterfaceModule.get("dim"); - - // set the digital channel to output mode. - // remember, the Adafruit sensor is actually two devices. - // It's an I2C sensor and it's also an LED that can be turned on or off. - cdim.setDigitalChannelMode(LED_CHANNEL, DigitalChannel.Mode.OUTPUT); - - // get a reference to our ColorSensor object. - sensorRGB = hardwareMap.colorSensor.get("sensor_color"); - - // turn the LED on in the beginning, just so user will know that the sensor is active. - cdim.setDigitalChannelState(LED_CHANNEL, bLedOn); - - // wait for the start button to be pressed. - waitForStart(); - - // loop and read the RGB data. - // Note we use opModeIsActive() as our loop condition because it is an interruptible method. - while (opModeIsActive()) { - - // check the status of the x button on gamepad. - bCurrState = gamepad1.x; - - // check for button-press state transitions. - if ((bCurrState == true) && (bCurrState != bPrevState)) { - - // button is transitioning to a pressed state. Toggle the LED. - bLedOn = !bLedOn; - cdim.setDigitalChannelState(LED_CHANNEL, bLedOn); - } - - // update previous state variable. - bPrevState = bCurrState; - - // convert the RGB values to HSV values. - Color.RGBToHSV((sensorRGB.red() * 255) / 800, (sensorRGB.green() * 255) / 800, (sensorRGB.blue() * 255) / 800, hsvValues); - - // send the info back to driver station using telemetry function. - telemetry.addData("LED", bLedOn ? "On" : "Off"); - telemetry.addData("Clear", sensorRGB.alpha()); - telemetry.addData("Red ", sensorRGB.red()); - telemetry.addData("Green", sensorRGB.green()); - telemetry.addData("Blue ", sensorRGB.blue()); - telemetry.addData("Hue", hsvValues[0]); - - // change the background color to match the color detected by the RGB sensor. - // pass a reference to the hue, saturation, and value array as an argument - // to the HSVToColor method. - relativeLayout.post(new Runnable() { - public void run() { - relativeLayout.setBackgroundColor(Color.HSVToColor(0xff, values)); - } - }); - - telemetry.update(); - } - - // Set the panel back to the default color - relativeLayout.post(new Runnable() { - public void run() { - relativeLayout.setBackgroundColor(Color.WHITE); - } - }); - } -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDIO.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDIO.java deleted file mode 100644 index 12d5d93db8..0000000000 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDIO.java +++ /dev/null @@ -1,107 +0,0 @@ -/* Copyright (c) 2017 FIRST. 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 FIRST 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 org.firstinspires.ftc.robotcontroller.external.samples; - -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.hardware.DeviceInterfaceModule; -import com.qualcomm.robotcore.hardware.DigitalChannel; - -/* - * This is an example LinearOpMode that shows how to use the digital inputs and outputs on the - * the Modern Robotics Device Interface Module. In addition, it shows how to use the Red and Blue LED - * - * This op mode assumes that there is a Device Interface Module attached, named 'dim'. - * On this DIM there is a digital input named 'digin' and an output named 'digout' - * - * To fully exercise this sample, connect pin 3 of the digin connector to pin 3 of the digout. - * Note: Pin 1 is indicated by the black stripe, so pin 3 is at the opposite end. - * - * The X button on the gamepad will be used to activate the digital output pin. - * The Red/Blue LED will be used to indicate the state of the digital input pin. - * Blue = false (0V), Red = true (5V) - * If the two pins are linked, the gamepad will change the LED color. - * - * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list -*/ -@TeleOp(name = "Sensor: DIM DIO", group = "Sensor") -@Disabled -public class SensorDIO extends LinearOpMode { - -final int BLUE_LED_CHANNEL = 0; -final int RED_LED_CHANNEL = 1; - - @Override - public void runOpMode() { - - boolean inputPin; // Input State - boolean outputPin; // Output State - DeviceInterfaceModule dim; // Device Object - DigitalChannel digIn; // Device Object - DigitalChannel digOut; // Device Object - - // get a reference to a Modern Robotics DIM, and IO channels. - dim = hardwareMap.get(DeviceInterfaceModule.class, "dim"); // Use generic form of device mapping - digIn = hardwareMap.get(DigitalChannel.class, "digin"); // Use generic form of device mapping - digOut = hardwareMap.get(DigitalChannel.class, "digout"); // Use generic form of device mapping - - digIn.setMode(DigitalChannel.Mode.INPUT); // Set the direction of each channel - digOut.setMode(DigitalChannel.Mode.OUTPUT); - - // wait for the start button to be pressed. - telemetry.addData(">", "Press play, and then user X button to set DigOut"); - telemetry.update(); - waitForStart(); - - while (opModeIsActive()) { - - outputPin = gamepad1.x ; // Set the output pin based on x button - digOut.setState(outputPin); - inputPin = digIn.getState(); // Read the input pin - - // Display input pin state on LEDs - if (inputPin) { - dim.setLED(RED_LED_CHANNEL, true); - dim.setLED(BLUE_LED_CHANNEL, false); - } - else { - dim.setLED(RED_LED_CHANNEL, false); - dim.setLED(BLUE_LED_CHANNEL, true); - } - - telemetry.addData("Output", outputPin ); - telemetry.addData("Input", inputPin ); - telemetry.addData("LED", inputPin ? "Red" : "Blue" ); - telemetry.update(); - } - } -} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRColor.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRColor.java index 513ace8aa7..efba7d17d3 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRColor.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRColor.java @@ -49,7 +49,7 @@ * You can use the X button on gamepad1 to toggle the LED on and off. * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list */ @TeleOp(name = "Sensor: MR Color", group = "Sensor") @Disabled diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java index e8df38f5c2..5425f0dbf2 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java @@ -49,7 +49,7 @@ * I2C channel and is configured with a name of "gyro". * * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this opmode to the Driver Station OpMode list + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list */ @TeleOp(name = "Sensor: MR Gyro", group = "Sensor") @Disabled @@ -83,7 +83,7 @@ public void runOpMode() { // A similar approach will work for the Gyroscope interface, if that's all you need. // Start calibrating the gyro. This takes a few seconds and is worth performing - // during the initialization phase at the start of each opMode. + // during the initialization phase at the start of each OpMode. telemetry.log().add("Gyro Calibrating. Do Not Move!"); modernRoboticsI2cGyro.calibrate(); diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md index 1808cd32f9..326978de80 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md @@ -27,15 +27,9 @@ Sensor: This is a Sample OpMode that shows how to use a specific sensor. It is not intended to drive a functioning robot, it is simply showing the minimal code required to read and display the sensor values. -Hardware: This is NOT an OpMode, but a helper class that is used to describe - one particular robot's hardware configuration: eg: For the K9 or Pushbot. - Look at any Pushbot sample to see how this can be used in an OpMode. - Teams can copy one of these to their team folder to create their own robot definition. - -Pushbot: This is a Sample OpMode that uses the Pushbot robot hardware as a base. - It may be used to provide some standard baseline Pushbot OpModes, or - to demonstrate how a particular sensor or concept can be used directly on the - Pushbot chassis. +Robot: This is a Sample OpMode that assumes a simple two-motor (differential) drive base. + It may be used to provide a common baseline driving OpMode, or + to demonstrate how a particular sensor or concept can be used to navigate. Concept: This is a sample OpMode that illustrates performing a specific function or concept. These may be complex, but their operation should be explained clearly in the comments, @@ -43,15 +37,9 @@ Concept: This is a sample OpMode that illustrates performing a specific function Each OpMode should try to only demonstrate a single concept so they are easy to locate based on their name. These OpModes may not produce a drivable robot. -Library: This is a class, or set of classes used to implement some strategy. - These will typically NOT implement a full OpMode. Instead they will be included - by an OpMode to provide some stand-alone capability. - After the prefix, other conventions will apply: * Sensor class names are constructed as: Sensor - Company - Type -* Hardware class names are constructed as: Hardware - Robot type -* Pushbot class names are constructed as: Pushbot - Mode - Action - OpModetype +* Robot class names are constructed as: Robot - Mode - Action - OpModetype * Concept class names are constructed as: Concept - Topic - OpModetype -* Library class names are constructed as: Library - Topic - OpModetype diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/sample_conventions.md b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/sample_conventions.md index 0f1aa131b5..45968ef939 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/sample_conventions.md +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/sample_conventions.md @@ -5,45 +5,35 @@ This document defines the FTC Sample OpMode and Class conventions. ### OpMode Name -A range of different samples classes will reside in the java/external/samples folder. +To gain a better understanding of how the samples are organized, and how to interpret the +naming system, it will help to understand the conventions that were used during their creation. -For ease of understanding, the class names will follow a naming convention which indicates -the purpose of each class. The prefix of the name will be one of the following: +To summarize: A range of different samples classes will reside in the java/external/samples. +The class names will follow a naming convention which indicates the purpose of each class. +The prefix of the name will be one of the following: -Basic: This is a minimally functional OpMode used to illustrate the skeleton/structure - of a particular style of OpMode. These are bare bones Tank Drive examples. +Basic: This is a minimally functional OpMode used to illustrate the skeleton/structure + of a particular style of OpMode. These are bare bones examples. Sensor: This is a Sample OpMode that shows how to use a specific sensor. It is not intended to drive a functioning robot, it is simply showing the minimal code required to read and display the sensor values. -Hardware: This is not an actual OpMode, but a helper class that is used to describe - one particular robot's hardware configuration: eg: For the K9 or Pushbot. - Look at any Pushbot sample to see how this can be used in an OpMode. - Teams can copy one of these to create their own robot definition. - -Pushbot: This is a Sample OpMode that uses the Pushbot robot hardware as a base. - It may be used to provide some standard baseline Pushbot opmodes, or - to demonstrate how a particular sensor or concept can be used directly on the - Pushbot chassis. +Robot: This is a Sample OpMode that assumes a simple two-motor (differential) drive base. + It may be used to provide a common baseline driving OpMode, or + to demonstrate how a particular sensor or concept can be used to navigate. Concept: This is a sample OpMode that illustrates performing a specific function or concept. These may be complex, but their operation should be explained clearly in the comments, or the comments should reference an external doc, guide or tutorial. Each OpMode should try to only demonstrate a single concept so they are easy to - locate based on their name. - -Library: This is a class, or set of classes used to implement some strategy. - These will typically NOT implement a full opmode. Instead they will be included - by an OpMode to provide some stand-alone capability. + locate based on their name. These OpModes may not produce a drivable robot. After the prefix, other conventions will apply: * Sensor class names should constructed as: Sensor - Company - Type -* Hardware class names should be constructed as: Hardware - Robot type -* Pushbot class names should be constructed as: Pushbot - Mode - Action - OpModetype +* Robot class names should be constructed as: Robot - Mode - Action - OpModetype * Concept class names should be constructed as: Concept - Topic - OpModetype -* Library class names should be constructed as: Library - Topic - OpModetype ### Sample OpMode Content/Style diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java index 9cff1d1cdf..8282f82e97 100644 --- a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java @@ -306,9 +306,9 @@ protected void onCreate(Bundle savedInstanceState) { preferencesHelper.writeBooleanPrefIfDifferent(context.getString(R.string.pref_rc_connected), true); preferencesHelper.getSharedPreferences().registerOnSharedPreferenceChangeListener(sharedPreferencesListener); - // Check if this RC app is from a later FTC season that what was installed previously + // Check if this RC app is from a later FTC season than what was installed previously int ftcSeasonYearOfPreviouslyInstalledRc = preferencesHelper.readInt(getString(R.string.pref_ftc_season_year_of_current_rc), 0); - int ftcSeasonYearOfCurrentlyInstalledRc = AppUtil.getInstance().getFtcSeasonYear(YearMonth.now()).getValue(); + int ftcSeasonYearOfCurrentlyInstalledRc = AppUtil.getInstance().getFtcSeasonYear(AppUtil.getInstance().getLocalSdkBuildMonth()).getValue(); if (ftcSeasonYearOfCurrentlyInstalledRc > ftcSeasonYearOfPreviouslyInstalledRc) { preferencesHelper.writeIntPrefIfDifferent(getString(R.string.pref_ftc_season_year_of_current_rc), ftcSeasonYearOfCurrentlyInstalledRc); // Since it's a new FTC season, we should reset certain settings back to their default values. @@ -395,10 +395,9 @@ public boolean onMenuItemClick(MenuItem item) { readNetworkType(); ServiceController.startService(FtcRobotControllerWatchdogService.class); bindToService(); - logPackageVersions(); - logDeviceSerialNumber(); - AndroidBoard.getInstance().logAndroidBoardInfo(); + RobotLog.logAppInfo(); RobotLog.logDeviceInfo(); + AndroidBoard.getInstance().logAndroidBoardInfo(); if (preferencesHelper.readBoolean(getString(R.string.pref_wifi_automute), false)) { initWifiMute(true); @@ -499,19 +498,6 @@ protected void unbindFromService() { } } - protected void logPackageVersions() { - RobotLog.logBuildConfig(com.qualcomm.ftcrobotcontroller.BuildConfig.class); - RobotLog.logBuildConfig(com.qualcomm.robotcore.BuildConfig.class); - RobotLog.logBuildConfig(com.qualcomm.hardware.BuildConfig.class); - RobotLog.logBuildConfig(com.qualcomm.ftccommon.BuildConfig.class); - RobotLog.logBuildConfig(com.google.blocks.BuildConfig.class); - RobotLog.logBuildConfig(org.firstinspires.inspection.BuildConfig.class); - } - - protected void logDeviceSerialNumber() { - RobotLog.ii(TAG, "Android device serial number: " + Device.getSerialNumberOrUnknown()); - } - protected void readNetworkType() { // Control hubs are always running the access point model. Everything else, for the time // being always runs the Wi-Fi Direct model. diff --git a/FtcRobotController/src/main/res/values/strings.xml b/FtcRobotController/src/main/res/values/strings.xml index 1aa7500232..6ea191ed0b 100644 --- a/FtcRobotController/src/main/res/values/strings.xml +++ b/FtcRobotController/src/main/res/values/strings.xml @@ -65,7 +65,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @style/AppThemeTealRC - pref_ftc_season_year_of_current_rc + pref_ftc_season_year_of_current_rc_new @string/packageNameRobotController diff --git a/FtcRobotController/src/main/res/xml/device_filter.xml b/FtcRobotController/src/main/res/xml/device_filter.xml index 7677dad219..7b75350c4a 100644 --- a/FtcRobotController/src/main/res/xml/device_filter.xml +++ b/FtcRobotController/src/main/res/xml/device_filter.xml @@ -37,7 +37,6 @@ https://developer.android.com/guide/topics/connectivity/usb/host - diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..88b776b680 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2014-2022 FIRST. 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 FIRST 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. + diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 833545f6ee..5d91c8fa04 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -15,6 +15,10 @@ apply from: '../build.common.gradle' apply from: '../build.dependencies.gradle' +android { + namespace = 'org.firstinspires.ftc.teamcode' +} + dependencies { implementation project(':FtcRobotController') annotationProcessor files('lib/OpModeAnnotationProcessor.jar') diff --git a/TeamCode/src/main/AndroidManifest.xml b/TeamCode/src/main/AndroidManifest.xml index 6c3420e3e1..3705b3150b 100644 --- a/TeamCode/src/main/AndroidManifest.xml +++ b/TeamCode/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md index 2f7e3a4aa5..4d1da42de0 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md @@ -14,31 +14,41 @@ Sample opmodes exist in the FtcRobotController module. To locate these samples, find the FtcRobotController module in the "Project/Android" tab. Expand the following tree elements: - FtcRobotController / java / org.firstinspires.ftc.robotcontroller / external / samples + FtcRobotController/java/org.firstinspires.ftc.robotcontroller/external/samples -A range of different samples classes can be seen in this folder. -The class names follow a naming convention which indicates the purpose of each class. -The full description of this convention is found in the samples/sample_convention.md file. +### Naming of Samples -A brief synopsis of the naming convention is given here: +To gain a better understanding of how the samples are organized, and how to interpret the +naming system, it will help to understand the conventions that were used during their creation. + +These conventions are described (in detail) in the sample_conventions.md file in this folder. + +To summarize: A range of different samples classes will reside in the java/external/samples. +The class names will follow a naming convention which indicates the purpose of each class. The prefix of the name will be one of the following: -* Basic: This is a minimally functional OpMode used to illustrate the skeleton/structure +Basic: This is a minimally functional OpMode used to illustrate the skeleton/structure of a particular style of OpMode. These are bare bones examples. -* Sensor: This is a Sample OpMode that shows how to use a specific sensor. - It is not intended as a functioning robot, it is simply showing the minimal code + +Sensor: This is a Sample OpMode that shows how to use a specific sensor. + It is not intended to drive a functioning robot, it is simply showing the minimal code required to read and display the sensor values. -* Hardware: This is not an actual OpMode, but a helper class that is used to describe - one particular robot's hardware devices: eg: for a Pushbot. Look at any - Pushbot sample to see how this can be used in an OpMode. - Teams can copy one of these to create their own robot definition. -* Pushbot: This is a Sample OpMode that uses the Pushbot robot structure as a base. -* Concept: This is a sample OpMode that illustrates performing a specific function or concept. + +Robot: This is a Sample OpMode that assumes a simple two-motor (differential) drive base. + It may be used to provide a common baseline driving OpMode, or + to demonstrate how a particular sensor or concept can be used to navigate. + +Concept: This is a sample OpMode that illustrates performing a specific function or concept. These may be complex, but their operation should be explained clearly in the comments, - or the header should reference an external doc, guide or tutorial. -* Library: This is a class, or set of classes used to implement some strategy. - These will typically NOT implement a full OpMode. Instead they will be included - by an OpMode to provide some stand-alone capability. + or the comments should reference an external doc, guide or tutorial. + Each OpMode should try to only demonstrate a single concept so they are easy to + locate based on their name. These OpModes may not produce a drivable robot. + +After the prefix, other conventions will apply: + +* Sensor class names are constructed as: Sensor - Company - Type +* Robot class names are constructed as: Robot - Mode - Action - OpModetype +* Concept class names are constructed as: Concept - Topic - OpModetype Once you are familiar with the range of samples available, you can choose one to be the basis for your own robot. In all cases, the desired sample(s) needs to be copied into @@ -50,7 +60,7 @@ This is done inside Android Studio directly, using the following steps: 2) Right click on the sample class and select "Copy" - 3) Expand the TeamCode / java folder + 3) Expand the TeamCode/java folder 4) Right click on the org.firstinspires.ftc.teamcode folder and select "Paste" diff --git a/build.common.gradle b/build.common.gradle index f85eeadcc1..ee89c46680 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -110,7 +110,6 @@ android { debug { debuggable true jniDebuggable true - renderscriptDebuggable true ndk { abiFilters "armeabi-v7a", "arm64-v8a" } diff --git a/build.dependencies.gradle b/build.dependencies.gradle index f994830df0..09e118ccfa 100644 --- a/build.dependencies.gradle +++ b/build.dependencies.gradle @@ -1,7 +1,6 @@ repositories { mavenCentral() google() // Needed for androidx - jcenter() // Needed for tensorflow-lite maven { url = 'https://maven.brott.dev/' } flatDir { dirs rootProject.file('libs') @@ -9,18 +8,17 @@ repositories { } dependencies { - implementation 'org.firstinspires.ftc:Inspection:7.1.0' - implementation 'org.firstinspires.ftc:Blocks:7.1.0' - implementation 'org.firstinspires.ftc:Tfod:7.1.0' - implementation 'org.firstinspires.ftc:RobotCore:7.1.0' - implementation 'org.firstinspires.ftc:RobotServer:7.1.0' - implementation 'org.firstinspires.ftc:OnBotJava:7.1.0' - implementation 'org.firstinspires.ftc:Hardware:7.1.0' - implementation 'org.firstinspires.ftc:FtcCommon:7.1.0' + implementation 'org.firstinspires.ftc:Inspection:8.0.0' + implementation 'org.firstinspires.ftc:Blocks:8.0.0' + implementation 'org.firstinspires.ftc:Tfod:8.0.0' + implementation 'org.firstinspires.ftc:RobotCore:8.0.0' + implementation 'org.firstinspires.ftc:RobotServer:8.0.0' + implementation 'org.firstinspires.ftc:OnBotJava:8.0.0' + implementation 'org.firstinspires.ftc:Hardware:8.0.0' + implementation 'org.firstinspires.ftc:FtcCommon:8.0.0' implementation 'org.tensorflow:tensorflow-lite-task-vision:0.2.0' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'org.firstinspires.ftc:gameAssets-FreightFrenzy:1.0.0' + implementation 'org.firstinspires.ftc:gameAssets-PowerPlay:1.0.0' - implementation 'com.acmerobotics.dashboard:dashboard:0.4.4' + implementation 'com.acmerobotics.dashboard:dashboard:0.4.5' } - diff --git a/build.gradle b/build.gradle index 87f05070ad..2ce4e6277a 100644 --- a/build.gradle +++ b/build.gradle @@ -4,18 +4,13 @@ * It is extraordinarily rare that you will ever need to edit this file. */ -configurations { - doc { transitive false } -} - buildscript { repositories { mavenCentral() google() - jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:7.2.0' } } @@ -25,7 +20,6 @@ allprojects { repositories { mavenCentral() google() - jcenter() } } @@ -36,34 +30,3 @@ repositories { dirs '../libs' } } - -dependencies { - doc 'org.firstinspires.ftc:Hardware:6.2.0' - doc 'org.firstinspires.ftc:RobotCore:6.2.0' - doc 'org.firstinspires.ftc:FtcCommon:6.2.0' - doc 'org.firstinspires.ftc:OnBotJava:6.2.0' - doc 'org.firstinspires.ftc:Inspection:6.2.0' -} - -task extractJavadoc { - doLast { - def componentIds = configurations.doc.incoming.resolutionResult.allDependencies.collect { it.selected.id } - - def result = dependencies.createArtifactResolutionQuery() - .forComponents(componentIds) - .withArtifacts(JvmLibrary, SourcesArtifact, JavadocArtifact) - .execute() - - for (component in result.resolvedComponents) { - component.getArtifacts(JavadocArtifact).each { artifact -> - def version = artifact.identifier.componentIdentifier.version - def libName = artifact.identifier.componentIdentifier.moduleIdentifier.name - copy { - from zipTree(artifact.file) - into "docs/$version/$libName/" - } - } - } - } -} - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 01b8bf6b1f99cad9213fc495b33ad5bbab8efd20..f3d88b1c2faf2fc91d853cd5d4242b5547257070 100644 GIT binary patch literal 58695 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 literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqeFT zAwqu@)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 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